From e1e6508fec6826261e55304d76780a9442218331 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Thu, 8 Dec 2005 07:13:07 +0000 Subject: [PATCH] Moving to root below branch label git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2005 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .classpath | 10 + .externalToolBuilders/JibX.launch | 29 + .project | 28 + config/alfresco/action-services-context.xml | 299 + config/alfresco/application-context.xml | 863 ++ .../authentication-services-context.xml | 282 + .../alfresco/authority-services-context.xml | 45 + .../alfresco/bootstrap/Alfresco-Tutorial.pdf | Bin 0 -> 3285790 bytes config/alfresco/bootstrap/categories.xml | 1099 +++ config/alfresco/bootstrap/descriptor.xml | 11 + config/alfresco/bootstrap/spaces.xml | 38 + config/alfresco/bootstrap/tutorial.xml | 21 + config/alfresco/cache-context.xml | 85 + config/alfresco/content-services-context.xml | 163 + .../alfresco/domain/hibernate-cfg.properties | 19 + config/alfresco/domain/transaction.properties | 9 + config/alfresco/extension/customModel.xml | 28 + config/alfresco/extension/exampleModel.xml | 79 + .../alfresco/extension/extension-context.xml | 18 + config/alfresco/file-servers.xml | 70 + config/alfresco/hibernate-context-old.xml | 71 + config/alfresco/hibernate-context.xml | 80 + config/alfresco/index-recovery-context.xml | 24 + .../messages/action-config.properties | 70 + .../messages/action-service.properties | 9 + .../messages/application-model.properties | 45 + .../messages/bootstrap-spaces.properties | 13 + .../messages/bootstrap-templates.properties | 14 + .../messages/bootstrap-tutorial.properties | 9 + .../alfresco/messages/coci-service.properties | 8 + .../messages/content-model.properties | 201 + .../messages/content-service.properties | 3 + .../messages/dictionary-model.properties | 34 + .../alfresco/messages/forum-model.properties | 19 + .../messages/permissions-service.properties | 1 + .../alfresco/messages/rule-config.properties | 4 + .../alfresco/messages/system-model.properties | 31 + .../messages/template-service.properties | 5 + .../messages/version-service.properties | 8 + config/alfresco/mimetype-map.xml | 341 + .../model-specific-services-context.xml | 17 + config/alfresco/model/applicationModel.xml | 119 + config/alfresco/model/contentModel.xml | 624 ++ .../model/dataTypeAnalyzers.properties | 17 + config/alfresco/model/dictionaryModel.xml | 96 + config/alfresco/model/forumModel.xml | 60 + config/alfresco/model/modelSchema.xsd | 212 + .../alfresco/model/permissionDefinitions.xml | 342 + config/alfresco/model/permissionSchema.dtd | 37 + config/alfresco/model/systemModel.xml | 114 + config/alfresco/network-protocol-context.xml | 73 + config/alfresco/node-services-context.xml | 115 + .../alfresco/ownable-services-context-old.xml | 6 + config/alfresco/ownable-services-context.xml | 13 + config/alfresco/public-services-context.xml | 864 ++ .../public-services-security-context-old.xml | 64 + .../public-services-security-context.xml | 611 ++ config/alfresco/repository.properties | 76 + config/alfresco/rule-services-context.xml | 153 + config/alfresco/scheduled-jobs-context.xml | 150 + config/alfresco/template-services-context.xml | 32 + .../content/examples/company_logos.ftl | 20 + .../templates/content/examples/doc_info.ftl | 17 + .../templates/content/examples/example.ftl | 47 + .../content/examples/localizable.ftl | 10 + .../templates/content/examples/my_docs.ftl | 20 + .../content/examples/my_pressreleases.ftl | 22 + .../templates/content/examples/my_spaces.ftl | 15 + .../templates/content/examples/my_summary.ftl | 8 + .../content/examples/translatable.ftl | 12 + .../content/examples/userhome_docs.ftl | 15 + .../templates/content_template_examples.xml | 213 + .../software_engineering_project.xml | 69 + .../alfresco/templates/system-overview.html | 18 + config/alfresco/version.properties | 14 + config/ehcache.xml | 161 + config/treecache.xml | 90 + project-build.xml | 27 + project-override.properties | 2 + project.properties | 2 + .../example/SimpleExampleWithContent.java | 161 + .../java/org/alfresco/filesys/CIFSServer.java | 240 + .../java/org/alfresco/filesys/FTPServer.java | 226 + .../org/alfresco/filesys/ftp/FTPCommand.java | 120 + .../alfresco/filesys/ftp/FTPDataSession.java | 369 + .../org/alfresco/filesys/ftp/FTPDate.java | 107 + .../filesys/ftp/FTPNetworkServer.java | 590 ++ .../org/alfresco/filesys/ftp/FTPPath.java | 580 ++ .../org/alfresco/filesys/ftp/FTPRequest.java | 173 + .../alfresco/filesys/ftp/FTPSessionList.java | 133 + .../alfresco/filesys/ftp/FTPSrvSession.java | 3420 ++++++++ .../filesys/ftp/InvalidPathException.java | 43 + .../alfresco/filesys/locking/FileLock.java | 187 + .../filesys/locking/FileLockException.java | 45 + .../filesys/locking/FileLockList.java | 246 + .../filesys/locking/FileUnlockException.java | 45 + .../locking/LockConflictException.java | 50 + .../filesys/locking/NotLockedException.java | 47 + .../netbios/NameTemplateException.java | 45 + .../filesys/netbios/NetBIOSDatagram.java | 672 ++ .../netbios/NetBIOSDatagramSocket.java | 172 + .../filesys/netbios/NetBIOSException.java | 43 + .../alfresco/filesys/netbios/NetBIOSName.java | 1049 +++ .../filesys/netbios/NetBIOSNameList.java | 198 + .../filesys/netbios/NetBIOSPacket.java | 1247 +++ .../filesys/netbios/NetBIOSSession.java | 1938 +++++ .../filesys/netbios/NetworkSettings.java | 202 + .../filesys/netbios/RFCNetBIOSProtocol.java | 59 + .../netbios/server/AddNameListener.java | 31 + .../netbios/server/NetBIOSNameEvent.java | 82 + .../netbios/server/NetBIOSNameServer.java | 1933 +++++ .../netbios/server/NetBIOSRequest.java | 239 + .../netbios/server/PacketReceiver.java | 35 + .../netbios/server/QueryNameListener.java | 34 + .../netbios/server/RemoteNameListener.java | 42 + .../filesys/netbios/win32/NetBIOS.java | 260 + .../filesys/netbios/win32/NetBIOSSocket.java | 400 + .../netbios/win32/NetBIOSSocketException.java | 37 + .../filesys/netbios/win32/Win32NetBIOS.java | 713 ++ .../filesys/netbios/win32/WinsockError.java | 223 + .../win32/WinsockNetBIOSException.java | 114 + .../filesys/server/NetworkServer.java | 547 ++ .../filesys/server/NetworkServerList.java | 159 + .../filesys/server/ServerListener.java | 41 + .../filesys/server/SessionListener.java | 47 + .../alfresco/filesys/server/SrvSession.java | 554 ++ .../filesys/server/SrvSessionList.java | 132 + .../server/auth/AlfrescoAuthenticator.java | 366 + .../filesys/server/auth/ClientInfo.java | 432 + .../server/auth/DefaultAuthenticator.java | 120 + .../server/auth/InvalidUserException.java | 43 + .../server/auth/LocalAuthenticator.java | 216 + .../server/auth/PasswordEncryptor.java | 417 + .../filesys/server/auth/SrvAuthenticator.java | 370 + .../filesys/server/auth/UserAccount.java | 331 + .../filesys/server/auth/UserAccountList.java | 195 + .../server/auth/acl/ACLParseException.java | 43 + .../server/auth/acl/AccessControl.java | 246 + .../server/auth/acl/AccessControlFactory.java | 91 + .../server/auth/acl/AccessControlList.java | 158 + .../server/auth/acl/AccessControlManager.java | 80 + .../server/auth/acl/AccessControlParser.java | 135 + .../auth/acl/DefaultAccessControlManager.java | 281 + .../server/auth/acl/DomainAccessControl.java | 69 + .../auth/acl/DomainAccessControlParser.java | 68 + .../auth/acl/InvalidACLTypeException.java | 43 + .../auth/acl/IpAddressAccessControl.java | 109 + .../acl/IpAddressAccessControlParser.java | 108 + .../auth/acl/ProtocolAccessControl.java | 118 + .../auth/acl/ProtocolAccessControlParser.java | 72 + .../server/auth/acl/UserAccessControl.java | 66 + .../auth/acl/UserAccessControlParser.java | 67 + .../IncompleteConfigurationException.java | 47 + .../config/InvalidConfigurationException.java | 47 + .../server/config/ServerConfiguration.java | 2930 +++++++ .../filesys/server/core/DeviceContext.java | 116 + .../server/core/DeviceContextException.java | 46 + .../filesys/server/core/DeviceInterface.java | 56 + .../core/InvalidDeviceInterfaceException.java | 46 + .../filesys/server/core/ShareMapper.java | 78 + .../filesys/server/core/ShareType.java | 135 + .../filesys/server/core/SharedDevice.java | 496 ++ .../filesys/server/core/SharedDeviceList.java | 253 + .../server/filesys/AccessDeniedException.java | 45 + .../filesys/server/filesys/AccessMode.java | 92 + .../server/filesys/DefaultShareMapper.java | 165 + .../server/filesys/DeviceAttribute.java | 35 + .../filesys/DirectoryNotEmptyException.java | 46 + .../server/filesys/DiskDeviceContext.java | 273 + .../server/filesys/DiskFullException.java | 47 + .../filesys/server/filesys/DiskInfo.java | 237 + .../filesys/server/filesys/DiskInterface.java | 228 + .../server/filesys/DiskSharedDevice.java | 85 + .../server/filesys/DiskSizeInterface.java | 36 + .../server/filesys/DiskVolumeInterface.java | 35 + .../filesys/server/filesys/FileAccess.java | 57 + .../filesys/server/filesys/FileAction.java | 114 + .../filesys/server/filesys/FileAttribute.java | 143 + .../server/filesys/FileExistsException.java | 45 + .../server/filesys/FileIdInterface.java | 44 + .../filesys/server/filesys/FileInfo.java | 946 +++ .../filesys/server/filesys/FileName.java | 626 ++ .../server/filesys/FileOfflineException.java | 47 + .../server/filesys/FileOpenParams.java | 749 ++ .../server/filesys/FileSharingException.java | 43 + .../filesys/server/filesys/FileStatus.java | 63 + .../filesys/server/filesys/FileSystem.java | 47 + .../server/filesys/HomeShareMapper.java | 330 + .../server/filesys/MediaOfflineException.java | 49 + .../filesys/server/filesys/NetworkFile.java | 836 ++ .../server/filesys/NetworkFileServer.java | 41 + .../filesys/server/filesys/NotifyChange.java | 170 + .../server/filesys/PathNotFoundException.java | 48 + .../filesys/server/filesys/SearchContext.java | 225 + .../filesys/server/filesys/ShareListener.java | 44 + .../filesys/server/filesys/SrvDiskInfo.java | 195 + .../filesys/TooManyConnectionsException.java | 45 + .../server/filesys/TooManyFilesException.java | 45 + .../server/filesys/TreeConnection.java | 367 + .../server/filesys/TreeConnectionHash.java | 119 + .../UnsupportedInfoLevelException.java | 45 + .../filesys/server/filesys/VolumeInfo.java | 169 + .../server/locking/FileLockListener.java | 57 + .../server/locking/FileLockingInterface.java | 38 + .../filesys/server/locking/LockManager.java | 85 + .../org/alfresco/filesys/smb/Capability.java | 48 + .../org/alfresco/filesys/smb/DataType.java | 35 + .../org/alfresco/filesys/smb/Dialect.java | 407 + .../alfresco/filesys/smb/DialectSelector.java | 182 + .../filesys/smb/DirectoryWatcher.java | 42 + .../alfresco/filesys/smb/FileInfoLevel.java | 89 + .../alfresco/filesys/smb/FindFirstNext.java | 31 + .../filesys/smb/InvalidUNCPathException.java | 46 + .../org/alfresco/filesys/smb/LockingAndX.java | 98 + .../org/alfresco/filesys/smb/NTIOCtl.java | 238 + .../java/org/alfresco/filesys/smb/NTTime.java | 98 + .../alfresco/filesys/smb/NetworkSession.java | 89 + .../org/alfresco/filesys/smb/PCShare.java | 582 ++ .../org/alfresco/filesys/smb/PacketType.java | 410 + .../org/alfresco/filesys/smb/Protocol.java | 53 + .../org/alfresco/filesys/smb/SMBDate.java | 145 + .../alfresco/filesys/smb/SMBDeviceType.java | 66 + .../alfresco/filesys/smb/SMBErrorText.java | 839 ++ .../alfresco/filesys/smb/SMBException.java | 92 + .../org/alfresco/filesys/smb/SMBStatus.java | 276 + .../org/alfresco/filesys/smb/SeekType.java | 29 + .../org/alfresco/filesys/smb/ServerType.java | 418 + .../org/alfresco/filesys/smb/SharingMode.java | 33 + .../org/alfresco/filesys/smb/TcpipSMB.java | 28 + .../alfresco/filesys/smb/TransactBuffer.java | 617 ++ .../filesys/smb/TransactionNames.java | 29 + .../java/org/alfresco/filesys/smb/WinNT.java | 67 + .../filesys/smb/dcerpc/DCEBuffer.java | 1896 +++++ .../smb/dcerpc/DCEBufferException.java | 43 + .../filesys/smb/dcerpc/DCECommand.java | 77 + .../filesys/smb/dcerpc/DCEDataPacker.java | 101 + .../filesys/smb/dcerpc/DCEException.java | 35 + .../alfresco/filesys/smb/dcerpc/DCEList.java | 332 + .../filesys/smb/dcerpc/DCEPipeType.java | 188 + .../filesys/smb/dcerpc/DCEReadable.java | 42 + .../filesys/smb/dcerpc/DCEReadableList.java | 35 + .../filesys/smb/dcerpc/DCEWriteable.java | 41 + .../filesys/smb/dcerpc/DCEWriteableList.java | 35 + .../filesys/smb/dcerpc/PolicyHandle.java | 215 + .../filesys/smb/dcerpc/PolicyHandleCache.java | 100 + .../alfresco/filesys/smb/dcerpc/Srvsvc.java | 95 + .../org/alfresco/filesys/smb/dcerpc/UUID.java | 407 + .../alfresco/filesys/smb/dcerpc/Wkssvc.java | 45 + .../smb/dcerpc/info/ConnectionInfo.java | 253 + .../smb/dcerpc/info/ConnectionInfoList.java | 58 + .../filesys/smb/dcerpc/info/ServerInfo.java | 338 + .../filesys/smb/dcerpc/info/ShareInfo.java | 610 ++ .../smb/dcerpc/info/ShareInfoList.java | 111 + .../filesys/smb/dcerpc/info/UserInfo.java | 773 ++ .../smb/dcerpc/info/WorkstationInfo.java | 337 + .../filesys/smb/dcerpc/server/DCEHandler.java | 42 + .../smb/dcerpc/server/DCEPipeFile.java | 260 + .../smb/dcerpc/server/DCEPipeHandler.java | 55 + .../smb/dcerpc/server/DCESrvPacket.java | 425 + .../smb/dcerpc/server/SrvsvcDCEHandler.java | 401 + .../smb/dcerpc/server/WkssvcDCEHandler.java | 178 + .../filesys/smb/mailslot/HostAnnouncer.java | 494 ++ .../filesys/smb/mailslot/MailSlot.java | 140 + .../smb/mailslot/SMBMailslotPacket.java | 985 +++ .../mailslot/TcpipNetBIOSHostAnnouncer.java | 229 + .../mailslot/Win32NetBIOSHostAnnouncer.java | 125 + .../mailslot/WinsockNetBIOSHostAnnouncer.java | 123 + .../filesys/smb/server/AdminSharedDevice.java | 38 + .../smb/server/CoreProtocolHandler.java | 3779 +++++++++ .../filesys/smb/server/CoreResumeKey.java | 221 + .../filesys/smb/server/DCERPCHandler.java | 799 ++ .../filesys/smb/server/DiskInfoPacker.java | 300 + .../org/alfresco/filesys/smb/server/Find.java | 31 + .../filesys/smb/server/FindInfoPacker.java | 945 +++ .../filesys/smb/server/IPCHandler.java | 658 ++ .../smb/server/LanManProtocolHandler.java | 2931 +++++++ .../filesys/smb/server/NTParameterPacker.java | 187 + .../filesys/smb/server/NTProtocolHandler.java | 7007 +++++++++++++++++ .../filesys/smb/server/NTTransPacket.java | 725 ++ .../smb/server/NamedPipeTransaction.java | 83 + .../smb/server/NetBIOSPacketHandler.java | 177 + .../server/NetBIOSSessionSocketHandler.java | 226 + .../alfresco/filesys/smb/server/OpenAndX.java | 32 + .../filesys/smb/server/PacketHandler.java | 299 + .../filesys/smb/server/PipeDevice.java | 27 + .../filesys/smb/server/PipeLanmanHandler.java | 636 ++ .../filesys/smb/server/ProtocolFactory.java | 82 + .../filesys/smb/server/ProtocolHandler.java | 198 + .../filesys/smb/server/QueryInfoPacker.java | 743 ++ .../filesys/smb/server/SMBPacket.java | 1035 +++ .../filesys/smb/server/SMBServer.java | 843 ++ .../filesys/smb/server/SMBSrvException.java | 99 + .../filesys/smb/server/SMBSrvPacket.java | 1756 +++++ .../filesys/smb/server/SMBSrvSession.java | 2063 +++++ .../smb/server/SMBSrvSessionState.java | 70 + .../filesys/smb/server/SMBSrvTransPacket.java | 838 ++ .../filesys/smb/server/SMBTransPacket.java | 391 + .../smb/server/SessionSocketHandler.java | 285 + .../filesys/smb/server/SrvSessionFactory.java | 33 + .../filesys/smb/server/SrvTransactBuffer.java | 189 + .../smb/server/TcpipSMBPacketHandler.java | 156 + .../server/TcpipSMBSessionSocketHandler.java | 171 + .../smb/server/notify/NotifyChangeEvent.java | 219 + .../server/notify/NotifyChangeEventList.java | 104 + .../server/notify/NotifyChangeHandler.java | 1119 +++ .../smb/server/notify/NotifyRequest.java | 595 ++ .../smb/server/notify/NotifyRequestList.java | 297 + .../smb/server/ntfs/NTFSStreamsInterface.java | 76 + .../filesys/smb/server/ntfs/StreamInfo.java | 415 + .../smb/server/ntfs/StreamInfoList.java | 168 + .../filesys/smb/server/repo/CifsHelper.java | 494 ++ .../smb/server/repo/CifsIntegrationTest.java | 78 + .../smb/server/repo/ContentContext.java | 137 + .../smb/server/repo/ContentDiskDriver.java | 1519 ++++ .../smb/server/repo/ContentDiskInterface.java | 44 + .../smb/server/repo/ContentNetworkFile.java | 430 + .../smb/server/repo/ContentSearchContext.java | 160 + .../filesys/smb/server/repo/FileState.java | 619 ++ .../smb/server/repo/FileStateTable.java | 426 + .../smb/server/win32/LanaListener.java | 36 + .../server/win32/Win32NetBIOSLanaMonitor.java | 423 + .../win32/Win32NetBIOSPacketHandler.java | 209 + .../Win32NetBIOSSessionSocketHandler.java | 1069 +++ .../win32/WinsockNetBIOSPacketHandler.java | 193 + .../org/alfresco/filesys/util/DataBuffer.java | 847 ++ .../org/alfresco/filesys/util/DataPacker.java | 778 ++ .../org/alfresco/filesys/util/HexDump.java | 259 + .../org/alfresco/filesys/util/IPAddress.java | 206 + .../org/alfresco/filesys/util/MemorySize.java | 214 + .../org/alfresco/filesys/util/StringList.java | 172 + .../org/alfresco/filesys/util/WildCard.java | 640 ++ .../java/org/alfresco/model/ContentModel.java | 198 + .../java/org/alfresco/model/ForumModel.java | 43 + .../action/ActionConditionDefinitionImpl.java | 68 + .../ActionConditionDefinitionImplTest.java | 67 + .../repo/action/ActionConditionImpl.java | 89 + .../repo/action/ActionConditionImplTest.java | 51 + .../repo/action/ActionDefinitionImpl.java | 68 + .../repo/action/ActionDefinitionImplTest.java | 61 + .../org/alfresco/repo/action/ActionImpl.java | 380 + .../alfresco/repo/action/ActionImplTest.java | 148 + .../org/alfresco/repo/action/ActionModel.java | 34 + .../repo/action/ActionServiceImpl.java | 1233 +++ .../repo/action/ActionServiceImplTest.java | 938 +++ .../alfresco/repo/action/ActionTestSuite.java | 69 + .../action/ActionTransactionListener.java | 124 + .../alfresco/repo/action/ActionsAspect.java | 161 + .../AsynchronousActionExecutionQueue.java | 44 + .../AsynchronousActionExecutionQueueImpl.java | 297 + ...seParameterizedItemDefinitionImplTest.java | 106 + .../action/BaseParameterizedItemImplTest.java | 110 + .../action/CommonResourceAbstractBase.java | 45 + .../repo/action/CompositeActionImpl.java | 126 + .../repo/action/CompositeActionImplTest.java | 108 + .../repo/action/ParameterDefinitionImpl.java | 106 + .../action/ParameterDefinitionImplTest.java | 72 + .../action/ParameterizedItemAbstractBase.java | 163 + .../ParameterizedItemDefinitionImpl.java | 213 + .../repo/action/ParameterizedItemImpl.java | 150 + .../repo/action/RuntimeActionService.java | 66 + .../org/alfresco/repo/action/actionModel.xml | 199 + .../evaluator/ActionConditionEvaluator.java | 47 + .../ActionConditionEvaluatorAbstractBase.java | 105 + .../evaluator/CompareMimeTypeEvaluator.java | 93 + .../CompareMimeTypeEvaluatorTest.java | 89 + .../ComparePropertyValueEvaluator.java | 266 + .../ComparePropertyValueEvaluatorTest.java | 507 ++ .../action/evaluator/HasAspectEvaluator.java | 84 + .../evaluator/HasAspectEvaluatorTest.java | 94 + .../evaluator/HasVersionHistoryEvaluator.java | 91 + .../action/evaluator/InCategoryEvaluator.java | 144 + .../action/evaluator/IsSubTypeEvaluator.java | 102 + .../evaluator/IsSubTypeEvaluatorTest.java | 95 + .../evaluator/NoConditionEvaluator.java | 54 + .../ComparePropertyValueOperation.java | 22 + .../compare/ContentPropertyName.java | 27 + .../compare/DatePropertyValueComparator.java | 102 + .../NumericPropertyValueComparator.java | 106 + .../compare/PropertyValueComparator.java | 49 + .../compare/TextPropertyValueComparator.java | 183 + .../repo/action/executer/ActionExecuter.java | 44 + .../executer/ActionExecuterAbstractBase.java | 103 + .../executer/AddFeaturesActionExecuter.java | 110 + .../AddFeaturesActionExecuterTest.java | 106 + .../executer/CheckInActionExecuter.java | 115 + .../executer/CheckOutActionExecuter.java | 113 + .../executer/CompositeActionExecuter.java | 78 + .../executer/ContentMetadataExtracter.java | 170 + .../ContentMetadataExtracterTest.java | 148 + .../action/executer/CopyActionExecuter.java | 100 + .../executer/CreateVersionActionExecuter.java | 75 + .../executer/ExporterActionExecuter.java | 249 + .../ImageTransformActionExecuter.java | 85 + .../executer/ImporterActionExecuter.java | 147 + .../executer/LinkCategoryActionExecuter.java | 128 + .../action/executer/MailActionExecuter.java | 106 + .../action/executer/MoveActionExecuter.java | 80 + .../SetPropertyValueActionExecuter.java | 83 + .../SetPropertyValueActionExecuterTest.java | 97 + .../SimpleWorkflowActionExecuter.java | 103 + .../SpecialiseTypeActionExecuter.java | 103 + .../SpecialiseTypeActionExecuterTest.java | 90 + .../executer/TransformActionExecuter.java | 247 + .../alfresco/repo/audit/AuditableAspect.java | 199 + .../repo/audit/AuditableAspectTest.java | 176 + .../org/alfresco/repo/cache/CacheTest.java | 220 + .../alfresco/repo/cache/EhCacheAdapter.java | 112 + .../org/alfresco/repo/cache/SimpleCache.java | 42 + .../repo/cache/TransactionalCache.java | 570 ++ .../repo/coci/CheckOutCheckInServiceImpl.java | 478 ++ .../coci/CheckOutCheckInServiceImplTest.java | 403 + .../alfresco/repo/coci/WorkingCopyAspect.java | 139 + .../configuration/ConfigurableService.java | 56 + .../ConfigurableServiceImpl.java | 78 + .../ConfigurableServiceImplTest.java | 104 + .../repo/content/AbstractContentAccessor.java | 373 + .../content/AbstractContentReadWriteTest.java | 624 ++ .../repo/content/AbstractContentReader.java | 332 + .../repo/content/AbstractContentStore.java | 115 + .../repo/content/AbstractContentWriter.java | 315 + .../repo/content/ContentServicePolicies.java | 39 + .../alfresco/repo/content/ContentStore.java | 134 + .../repo/content/ContentStoreCleanupJob.java | 117 + .../content/ContentStoreCleanupJobTest.java | 114 + .../alfresco/repo/content/MimetypeMap.java | 250 + .../repo/content/MimetypeMapTest.java | 58 + .../repo/content/RandomAccessContent.java | 50 + .../repo/content/RoutingContentService.java | 384 + .../content/RoutingContentServiceTest.java | 585 ++ .../content/filestore/FileContentReader.java | 235 + .../content/filestore/FileContentStore.java | 319 + .../filestore/FileContentStoreTest.java | 93 + .../content/filestore/FileContentWriter.java | 223 + .../repo/content/filestore/FileIOTest.java | 114 + .../metadata/AbstractMetadataExtracter.java | 97 + .../AbstractMetadataExtracterTest.java | 128 + .../metadata/HtmlMetadataExtracter.java | 173 + .../metadata/HtmlMetadataExtracterTest.java | 60 + .../metadata/MP3MetadataExtracter.java | 245 + .../content/metadata/MetadataExtracter.java | 72 + .../metadata/MetadataExtracterRegistry.java | 180 + .../metadata/OfficeMetadataExtracter.java | 108 + .../metadata/OfficeMetadataExtracterTest.java | 60 + .../OpenDocumentMetadataExtracter.java | 95 + .../metadata/PdfBoxMetadataExtracter.java | 91 + .../metadata/PdfBoxMetadataExtracterTest.java | 43 + .../metadata/StringMetadataExtracter.java | 58 + .../metadata/UnoMetadataExtracter.java | 205 + .../metadata/UnoMetadataExtracterTest.java | 79 + .../transform/AbstractContentTransformer.java | 242 + .../AbstractContentTransformerTest.java | 214 + .../BinaryPassThroughContentTransformer.java | 74 + ...naryPassThroughContentTransformerTest.java | 59 + .../transform/ComplexContentTransformer.java | 149 + .../ComplexContentTransformerTest.java | 89 + .../transform/CompoundContentTransformer.java | 248 + .../content/transform/ContentTransformer.java | 84 + .../transform/ContentTransformerRegistry.java | 362 + .../ContentTransformerRegistryTest.java | 237 + .../HtmlParserContentTransformer.java | 76 + .../HtmlParserContentTransformerTest.java | 58 + .../transform/PdfBoxContentTransformer.java | 87 + .../PdfBoxContentTransformerTest.java | 55 + .../transform/PoiHssfContentTransformer.java | 251 + .../PoiHssfContentTransformerTest.java | 90 + .../RuntimeExecutableContentTransformer.java | 287 + ...ntimeExecutableContentTransformerTest.java | 82 + .../StringExtractingContentTransformer.java | 141 + ...tringExtractingContentTransformerTest.java | 162 + .../TextMiningContentTransformer.java | 86 + .../TextMiningContentTransformerTest.java | 90 + .../transform/UnoContentTransformer.java | 279 + .../transform/UnoContentTransformerTest.java | 71 + ...AbstractImageMagickContentTransformer.java | 256 + .../magick/ImageMagickContentTransformer.java | 110 + .../ImageMagickContentTransformerTest.java | 67 + .../magick/JMagickContentTransformer.java | 57 + .../magick/JMagickContentTransformerTest.java | 63 + .../content/transform/magick/alfresco.gif | Bin 0 -> 4229 bytes .../alfresco/repo/copy/CopyServiceImpl.java | 757 ++ .../repo/copy/CopyServiceImplTest.java | 707 ++ .../repo/copy/CopyServicePolicies.java | 72 + .../descriptor/DescriptorServiceImpl.java | 467 ++ .../descriptor/DescriptorServiceTest.java | 103 + .../repo/dictionary/CompiledModel.java | 366 + .../repo/dictionary/DelegateModelQuery.java | 151 + .../repo/dictionary/DictionaryBootstrap.java | 105 + .../repo/dictionary/DictionaryComponent.java | 289 + .../repo/dictionary/DictionaryDAO.java | 82 + .../repo/dictionary/DictionaryDAOImpl.java | 288 + .../repo/dictionary/DictionaryDAOTest.java | 153 + .../repo/dictionary/DictionaryModelType.java | 279 + .../dictionary/DictionaryModelTypeTest.java | 227 + .../DictionaryNamespaceComponent.java | 104 + .../DictionaryRepositoryBootstrap.java | 296 + .../DictionaryRepositoryBootstrapTest.java | 241 + .../dictionary/M2AnonymousTypeDefinition.java | 189 + .../alfresco/repo/dictionary/M2Aspect.java | 32 + .../repo/dictionary/M2AspectDefinition.java | 70 + .../repo/dictionary/M2Association.java | 37 + .../dictionary/M2AssociationDefinition.java | 232 + .../repo/dictionary/M2ChildAssociation.java | 66 + .../M2ChildAssociationDefinition.java | 62 + .../org/alfresco/repo/dictionary/M2Class.java | 233 + .../repo/dictionary/M2ClassAssociation.java | 189 + .../repo/dictionary/M2ClassDefinition.java | 408 + .../alfresco/repo/dictionary/M2DataType.java | 100 + .../repo/dictionary/M2DataTypeDefinition.java | 151 + .../org/alfresco/repo/dictionary/M2Label.java | 75 + .../org/alfresco/repo/dictionary/M2Model.java | 389 + .../repo/dictionary/M2ModelDefinition.java | 93 + .../alfresco/repo/dictionary/M2Namespace.java | 60 + .../alfresco/repo/dictionary/M2Property.java | 196 + .../repo/dictionary/M2PropertyDefinition.java | 265 + .../repo/dictionary/M2PropertyOverride.java | 73 + .../org/alfresco/repo/dictionary/M2Type.java | 32 + .../repo/dictionary/M2TypeDefinition.java | 69 + .../org/alfresco/repo/dictionary/M2XML.java | 72 + .../alfresco/repo/dictionary/ModelQuery.java | 92 + .../repo/dictionary/NamespaceDAO.java | 61 + .../repo/dictionary/NamespaceDAOImpl.java | 124 + .../alfresco/repo/dictionary/TestModel.java | 82 + .../dictionarydaotest_model.properties | 12 + .../dictionary/dictionarydaotest_model.xml | 148 + .../alfresco/repo/dictionary/m2binding.xml | 134 + .../org/alfresco/repo/domain/ChildAssoc.java | 94 + .../java/org/alfresco/repo/domain/Node.java | 92 + .../org/alfresco/repo/domain/NodeAssoc.java | 66 + .../org/alfresco/repo/domain/NodeKey.java | 123 + .../org/alfresco/repo/domain/NodeStatus.java | 47 + .../alfresco/repo/domain/PropertyValue.java | 606 ++ .../java/org/alfresco/repo/domain/Store.java | 54 + .../org/alfresco/repo/domain/StoreKey.java | 98 + .../alfresco/repo/domain/VersionCount.java | 59 + .../repo/domain/hibernate/ChildAssocImpl.java | 229 + .../domain/hibernate/HibernateNodeTest.java | 465 ++ .../repo/domain/hibernate/Node.hbm.xml | 333 + .../repo/domain/hibernate/NodeAssocImpl.java | 155 + .../repo/domain/hibernate/NodeImpl.java | 228 + .../repo/domain/hibernate/NodeStatusImpl.java | 94 + .../repo/domain/hibernate/QNameUserType.java | 111 + .../repo/domain/hibernate/Store.hbm.xml | 35 + .../repo/domain/hibernate/StoreImpl.java | 106 + .../domain/hibernate/VersionCount.hbm.xml | 25 + .../domain/hibernate/VersionCountImpl.java | 111 + .../exporter/ACPExportPackageHandler.java | 232 + .../repo/exporter/ChainedExporter.java | 302 + .../repo/exporter/ExporterComponent.java | 571 ++ .../repo/exporter/ExporterComponentTest.java | 220 + .../repo/exporter/ExporterCrawler.java | 39 + .../exporter/FileExportPackageHandler.java | 178 + .../alfresco/repo/exporter/URLExporter.java | 238 + .../repo/exporter/ViewXMLExporter.java | 674 ++ .../importer/ACPImportPackageHandler.java | 155 + .../repo/importer/DefaultContentHandler.java | 153 + .../importer/FileImportPackageHandler.java | 125 + .../alfresco/repo/importer/FileImporter.java | 67 + .../repo/importer/FileImporterException.java | 39 + .../repo/importer/FileImporterImpl.java | 294 + .../repo/importer/FileImporterTest.java | 313 + .../repo/importer/ImportContentHandler.java | 15 + .../alfresco/repo/importer/ImportNode.java | 76 + .../alfresco/repo/importer/ImportParent.java | 41 + .../org/alfresco/repo/importer/Importer.java | 76 + .../repo/importer/ImporterBootstrap.java | 507 ++ .../repo/importer/ImporterComponent.java | 1008 +++ .../repo/importer/ImporterComponentTest.java | 118 + .../org/alfresco/repo/importer/Parser.java | 42 + .../repo/importer/importercomponent_test.xml | 74 + .../importer/importercomponent_testfile.txt | 1 + .../repo/importer/view/ElementContext.java | 79 + .../repo/importer/view/MetaDataContext.java | 83 + .../repo/importer/view/NodeContext.java | 341 + .../repo/importer/view/NodeItemContext.java | 52 + .../repo/importer/view/ParentContext.java | 131 + .../repo/importer/view/ViewParser.java | 634 ++ .../repo/lock/LockBehaviourImplTest.java | 345 + .../alfresco/repo/lock/LockServiceImpl.java | 567 ++ .../repo/lock/LockServiceImplTest.java | 411 + .../org/alfresco/repo/lock/LockTestSuite.java | 32 + .../filefolder/FileFolderServiceImpl.java | 768 ++ .../filefolder/FileFolderServiceImplTest.java | 482 ++ .../repo/model/filefolder/FileInfoImpl.java | 97 + .../java/org/alfresco/repo/model/package.html | 8 + .../repo/node/AbstractNodeServiceImpl.java | 626 ++ .../repo/node/BaseNodeServiceTest.java | 1445 ++++ .../repo/node/BaseNodeServiceTest_model.xml | 294 + .../repo/node/ConcurrentNodeServiceTest.java | 221 + .../repo/node/NodeServicePolicies.java | 254 + .../repo/node/PerformanceNodeServiceTest.java | 259 + .../repo/node/ReferenceableAspect.java | 80 + .../repo/node/db/DbNodeServiceImpl.java | 1275 +++ .../repo/node/db/DbNodeServiceImplTest.java | 260 + .../alfresco/repo/node/db/NodeDaoService.java | 179 + .../HibernateNodeDaoServiceImpl.java | 507 ++ .../node/index/FtsIndexRecoveryComponent.java | 137 + .../index/FtsIndexRecoveryComponentTest.java | 66 + .../repo/node/index/IndexRecovery.java | 30 + .../repo/node/index/IndexRecoveryJob.java | 33 + .../alfresco/repo/node/index/NodeIndexer.java | 125 + .../repo/node/index/NodeIndexerTest.java | 158 + .../integrity/AbstractIntegrityEvent.java | 193 + ...AssocSourceMultiplicityIntegrityEvent.java | 157 + .../AssocSourceTypeIntegrityEvent.java | 143 + ...AssocTargetMultiplicityIntegrityEvent.java | 157 + .../AssocTargetRoleIntegrityEvent.java | 146 + .../AssocTargetTypeIntegrityEvent.java | 143 + .../repo/node/integrity/IntegrityChecker.java | 643 ++ .../repo/node/integrity/IntegrityEvent.java | 38 + .../node/integrity/IntegrityEventTest.java | 79 + .../node/integrity/IntegrityException.java | 47 + .../repo/node/integrity/IntegrityRecord.java | 79 + .../repo/node/integrity/IntegrityTest.java | 385 + .../node/integrity/IntegrityTest_model.xml | 140 + .../integrity/PropertiesIntegrityEvent.java | 142 + .../repo/ownable/impl/OwnableServiceImpl.java | 111 + .../ownable/impl/OwnableServiceNOOPImpl.java | 57 + .../repo/ownable/impl/OwnableServiceTest.java | 176 + .../repo/policy/AssociationPolicy.java | 28 + .../policy/AssociationPolicyDelegate.java | 207 + .../org/alfresco/repo/policy/Behaviour.java | 55 + .../repo/policy/BehaviourBinding.java | 38 + .../repo/policy/BehaviourChangeObserver.java | 36 + .../repo/policy/BehaviourDefinition.java | 58 + .../alfresco/repo/policy/BehaviourFilter.java | 100 + .../repo/policy/BehaviourFilterImpl.java | 224 + .../alfresco/repo/policy/BehaviourIndex.java | 62 + .../alfresco/repo/policy/BehaviourMap.java | 106 + .../repo/policy/CachedPolicyFactory.java | 251 + .../repo/policy/ClassBehaviourBinding.java | 135 + .../repo/policy/ClassBehaviourIndex.java | 216 + .../policy/ClassFeatureBehaviourBinding.java | 157 + .../org/alfresco/repo/policy/ClassPolicy.java | 27 + .../repo/policy/ClassPolicyDelegate.java | 186 + .../alfresco/repo/policy/JavaBehaviour.java | 261 + .../java/org/alfresco/repo/policy/Policy.java | 33 + .../alfresco/repo/policy/PolicyComponent.java | 178 + .../repo/policy/PolicyComponentImpl.java | 666 ++ .../repo/policy/PolicyComponentTest.java | 588 ++ .../repo/policy/PolicyDefinition.java | 52 + .../alfresco/repo/policy/PolicyException.java | 39 + .../alfresco/repo/policy/PolicyFactory.java | 234 + .../org/alfresco/repo/policy/PolicyList.java | 30 + .../org/alfresco/repo/policy/PolicyScope.java | 449 ++ .../org/alfresco/repo/policy/PolicyType.java | 31 + .../alfresco/repo/policy/PropertyPolicy.java | 26 + .../repo/policy/PropertyPolicyDelegate.java | 208 + .../repo/policy/ServiceBehaviourBinding.java | 81 + .../repo/policy/policycomponenttest_model.xml | 73 + .../org/alfresco/repo/rule/BaseRuleTest.java | 215 + .../org/alfresco/repo/rule/RuleCache.java | 40 + .../java/org/alfresco/repo/rule/RuleImpl.java | 89 + .../org/alfresco/repo/rule/RuleModel.java | 21 + .../repo/rule/RuleServiceCoverageTest.java | 1417 ++++ .../alfresco/repo/rule/RuleServiceImpl.java | 956 +++ .../repo/rule/RuleServiceImplTest.java | 625 ++ .../org/alfresco/repo/rule/RuleTestSuite.java | 46 + .../repo/rule/RuleTransactionListener.java | 115 + .../org/alfresco/repo/rule/RuleTypeImpl.java | 161 + .../alfresco/repo/rule/RuleTypeImplTest.java | 137 + .../org/alfresco/repo/rule/RulesAspect.java | 159 + .../repo/rule/RuntimeRuleService.java | 35 + .../java/org/alfresco/repo/rule/ruleModel.xml | 55 + .../ruletrigger/CreateNodeRuleTrigger.java | 82 + .../repo/rule/ruletrigger/RuleTrigger.java | 39 + .../ruletrigger/RuleTriggerAbstractBase.java | 124 + .../rule/ruletrigger/RuleTriggerTest.java | 265 + .../SingleAssocRefPolicyRuleTrigger.java | 63 + .../SingleChildAssocRefPolicyRuleTrigger.java | 92 + .../SingleNodeRefPolicyRuleTrigger.java | 81 + .../repo/search/AbstractResultSet.java | 78 + .../repo/search/AbstractResultSetRow.java | 112 + .../search/AbstractResultSetRowIterator.java | 133 + .../search/AbstractSearcherComponent.java | 80 + .../alfresco/repo/search/CannedQueryDef.java | 77 + .../repo/search/CannedQueryDefImpl.java | 162 + .../repo/search/DocumentNavigator.java | 439 ++ .../alfresco/repo/search/EmptyResultSet.java | 90 + .../org/alfresco/repo/search/ISO9075.java | 226 + .../org/alfresco/repo/search/ISO9075Test.java | 73 + .../org/alfresco/repo/search/Indexer.java | 105 + .../repo/search/IndexerAndSearcher.java | 57 + .../IndexerAndSearcherFactoryException.java | 53 + .../repo/search/IndexerComponent.java | 83 + .../repo/search/IndexerException.java | 39 + .../repo/search/NodeServiceXPath.java | 703 ++ .../alfresco/repo/search/QueryCollection.java | 69 + .../repo/search/QueryCollectionImpl.java | 155 + .../repo/search/QueryParameterDefImpl.java | 165 + .../repo/search/QueryParameterRefImpl.java | 80 + .../repo/search/QueryRegisterComponent.java | 72 + .../search/QueryRegisterComponentImpl.java | 160 + .../search/QueryRegisterComponentTest.java | 66 + .../repo/search/ResultSetRowIterator.java | 32 + .../repo/search/SearcherComponent.java | 108 + .../repo/search/SearcherComponentTest.java | 1136 +++ .../repo/search/SearcherException.java | 53 + .../repo/search/impl/JCR170Searcher.java | 59 + .../repo/search/impl/NoActionIndexer.java | 65 + .../repo/search/impl/NodeSearcher.java | 279 + .../repo/search/impl/lucene/CharStream.java | 110 + .../impl/lucene/ClosingIndexSearcher.java | 55 + .../search/impl/lucene/DebugXPathHandler.java | 267 + .../search/impl/lucene/FastCharStream.java | 122 + .../lucene/FilterIndexReaderByNodeRefs.java | 210 + .../repo/search/impl/lucene/Lockable.java | 34 + .../search/impl/lucene/LuceneAnalyser.java | 142 + .../repo/search/impl/lucene/LuceneBase.java | 1019 +++ .../lucene/LuceneCategoryServiceImpl.java | 311 + .../impl/lucene/LuceneCategoryTest.java | 672 ++ .../repo/search/impl/lucene/LuceneConfig.java | 36 + .../LuceneIndexBackupComponentTest.java | 79 + .../impl/lucene/LuceneIndexException.java | 45 + .../search/impl/lucene/LuceneIndexer.java | 48 + .../impl/lucene/LuceneIndexerAndSearcher.java | 27 + .../LuceneIndexerAndSearcherFactory.java | 1072 +++ .../search/impl/lucene/LuceneIndexerImpl.java | 1841 +++++ .../search/impl/lucene/LuceneQueryParser.java | 340 + .../search/impl/lucene/LuceneResultSet.java | 152 + .../impl/lucene/LuceneResultSetRow.java | 135 + .../lucene/LuceneResultSetRowIterator.java | 50 + .../search/impl/lucene/LuceneSearcher.java | 28 + .../impl/lucene/LuceneSearcherImpl.java | 652 ++ .../repo/search/impl/lucene/LuceneTest.java | 3133 ++++++++ .../search/impl/lucene/LuceneTest_model.xml | 187 + .../impl/lucene/LuceneXPathHandler.java | 488 ++ .../search/impl/lucene/ParseException.java | 192 + .../QueryParameterisationException.java | 39 + .../repo/search/impl/lucene/QueryParser.java | 1206 +++ .../repo/search/impl/lucene/QueryParser.jj | 826 ++ .../impl/lucene/QueryParserConstants.java | 78 + .../impl/lucene/QueryParserTokenManager.java | 1081 +++ .../repo/search/impl/lucene/Token.java | 97 + .../search/impl/lucene/TokenMgrError.java | 133 + .../lucene/analysis/CategoryAnalyser.java | 44 + .../impl/lucene/analysis/DateAnalyser.java | 37 + .../impl/lucene/analysis/DateTokenFilter.java | 66 + .../impl/lucene/analysis/DoubleAnalyser.java | 42 + .../lucene/analysis/DoubleTokenFilter.java | 60 + .../impl/lucene/analysis/FloatAnalyser.java | 41 + .../lucene/analysis/FloatTokenFilter.java | 60 + .../impl/lucene/analysis/IntegerAnalyser.java | 41 + .../lucene/analysis/IntegerTokenFilter.java | 60 + .../impl/lucene/analysis/LongAnalyser.java | 42 + .../impl/lucene/analysis/LongTokenFilter.java | 60 + .../impl/lucene/analysis/NumericEncoder.java | 185 + .../lucene/analysis/NumericEncodingTest.java | 186 + .../impl/lucene/analysis/PathAnalyser.java | 37 + .../impl/lucene/analysis/PathTokenFilter.java | 255 + .../impl/lucene/analysis/PathTokeniser.java | 46 + .../impl/lucene/fts/FTSIndexerAware.java | 25 + .../impl/lucene/fts/FTSIndexerException.java | 47 + .../search/impl/lucene/fts/FTSIndexerJob.java | 43 + .../lucene/fts/FullTextSearchIndexer.java | 35 + .../lucene/fts/FullTextSearchIndexerImpl.java | 210 + .../AbsoluteStructuredFieldPosition.java | 106 + .../AbstractStructuredFieldPosition.java | 124 + .../query/AnyStructuredFieldPosition.java | 91 + .../lucene/query/CachingTermPositions.java | 180 + .../impl/lucene/query/ContainerScorer.java | 553 ++ .../search/impl/lucene/query/DeltaReader.java | 238 + ...cendantAndSelfStructuredFieldPosition.java | 43 + .../search/impl/lucene/query/LeafScorer.java | 907 +++ .../search/impl/lucene/query/PathQuery.java | 341 + .../search/impl/lucene/query/PathScorer.java | 183 + .../RelativeStructuredFieldPosition.java | 94 + .../SelfAxisStructuredFieldPosition.java | 52 + .../lucene/query/StructuredFieldPosition.java | 114 + .../lucene/query/StructuredFieldTerm.java | 55 + .../results/ChildAssocRefResultSet.java | 98 + .../results/ChildAssocRefResultSetRow.java | 56 + .../ChildAssocRefResultSetRowIterator.java | 43 + .../search/results/DetachedResultSet.java | 69 + .../search/results/DetachedResultSetRow.java | 69 + .../search/transaction/LuceneIndexLock.java | 71 + .../LuceneTransactionException.java | 37 + .../search/transaction/SimpleTransaction.java | 205 + .../transaction/SimpleTransactionManager.java | 109 + .../repo/search/transaction/XidException.java | 47 + .../search/transaction/XidTransaction.java | 25 + .../AbstractAuthenticationComponent.java | 230 + ...atedAuthenticationPassthroughProvider.java | 51 + .../AuthenticationComponent.java | 104 + .../AuthenticationComponentImpl.java | 100 + .../AuthenticationException.java | 44 + .../AuthenticationServiceImpl.java | 134 + .../authentication/AuthenticationTest.java | 711 ++ .../DefaultMutableAuthenticationDao.java | 293 + .../InMemoryTicketComponentImpl.java | 204 + .../authentication/MD4PasswordEncoder.java | 30 + .../MD4PasswordEncoderImpl.java | 139 + .../MutableAuthenticationDao.java | 205 + .../security/authentication/NTLMMode.java | 22 + .../RepositoryAuthenticationDao.java | 510 ++ .../authentication/TicketComponent.java | 66 + .../TicketExpiredException.java | 37 + .../security/authentication/userModel.xml | 90 + .../authority/SimpleAuthorityServiceImpl.java | 207 + .../authority/SimpleAuthorityServiceTest.java | 140 + .../permissions/AccessDeniedException.java | 44 + .../permissions/AuthorityReference.java | 27 + .../permissions/DynamicAuthority.java | 46 + .../permissions/NodePermissionEntry.java | 51 + .../security/permissions/PermissionEntry.java | 76 + .../permissions/PermissionReference.java | 44 + .../permissions/PermissionServiceSPI.java | 141 + .../dynamic/LockOwnerDynamicAuthority.java | 63 + .../LockOwnerDynamicAuthorityTest.java | 216 + .../dynamic/OwnerDynamicAuthority.java | 58 + .../impl/AbstractNodePermissionEntry.java | 59 + .../impl/AbstractPermissionEntry.java | 76 + .../impl/AbstractPermissionReference.java | 63 + .../impl/AbstractPermissionTest.java | 166 + .../impl/AlwaysProceedMethodInterceptor.java | 35 + .../ExceptionTranslatorMethodInterceptor.java | 45 + .../security/permissions/impl/ModelDAO.java | 129 + .../impl/PermissionReferenceImpl.java | 52 + .../impl/PermissionServiceImpl.java | 1042 +++ .../impl/PermissionServiceTest.java | 1909 +++++ .../permissions/impl/PermissionsDAO.java | 132 + .../permissions/impl/RequiredPermission.java | 53 + .../impl/SimpleNodePermissionEntry.java | 76 + .../impl/SimplePermissionEntry.java | 92 + .../impl/SimplePermissionReference.java | 56 + .../ACLEntryAfterInvocationProvider.java | 635 ++ .../acegi/ACLEntryAfterInvocationTest.java | 884 +++ .../permissions/impl/acegi/ACLEntryVoter.java | 402 + .../impl/acegi/ACLEntryVoterException.java | 39 + .../impl/acegi/ACLEntryVoterTest.java | 805 ++ .../impl/acegi/FilteringResultSet.java | 247 + .../impl/acegi/FilteringResultSetTest.java | 122 + .../impl/acegi/MethodSecurityInterceptor.java | 30 + .../hibernate/HibernatePermissionTest.java | 209 + .../hibernate/HibernatePermissionsDAO.java | 421 + .../impl/hibernate/NodePermissionEntry.java | 70 + .../hibernate/NodePermissionEntryImpl.java | 117 + .../impl/hibernate/Permission.hbm.xml | 143 + .../impl/hibernate/PermissionEntry.java | 73 + .../impl/hibernate/PermissionEntryImpl.java | 181 + .../impl/hibernate/PermissionReference.java | 69 + .../hibernate/PermissionReferenceImpl.java | 98 + .../permissions/impl/hibernate/Recipient.java | 48 + .../impl/hibernate/RecipientImpl.java | 88 + .../impl/model/AbstractPermission.java | 147 + .../impl/model/DynamicPermission.java | 49 + .../impl/model/GlobalPermissionEntry.java | 88 + .../impl/model/ModelPermissionEntry.java | 142 + .../impl/model/NodePermission.java | 106 + .../permissions/impl/model/Permission.java | 157 + .../impl/model/PermissionGroup.java | 195 + .../impl/model/PermissionModel.java | 944 +++ .../impl/model/PermissionModelException.java | 44 + .../impl/model/PermissionModelTest.java | 55 + .../permissions/impl/model/PermissionSet.java | 110 + .../impl/model/XMLModelInitialisable.java | 30 + .../noop/PermissionServiceNOOPImpl.java | 219 + .../repo/security/person/PersonException.java | 54 + .../security/person/PersonServiceImpl.java | 323 + .../repo/security/person/PersonTest.java | 258 + .../repo/service/BeanServiceDescriptor.java | 103 + .../service/ServiceDescriptorAdvisor.java | 43 + .../ServiceDescriptorAdvisorFactory.java | 84 + .../service/ServiceDescriptorMetaData.java | 42 + .../repo/service/ServiceDescriptorMixin.java | 74 + .../service/ServiceDescriptorRegistry.java | 307 + .../ServiceDescriptorRegistryTest.java | 188 + .../repo/service/StoreRedirector.java | 34 + .../service/StoreRedirectorProxyFactory.java | 282 + .../StoreRedirectorProxyFactoryTest.java | 210 + .../service/serviceregistrytest_model.xml | 8 + .../alfresco/repo/service/testredirector.xml | 35 + .../alfresco/repo/service/testregistry.xml | 111 + .../repo/template/BasePathResultsMap.java | 97 + .../template/ClassPathRepoTemplateLoader.java | 247 + .../template/ClassPathTemplateLoader.java | 37 + .../repo/template/DateCompareMethod.java | 75 + .../repo/template/FreeMarkerProcessor.java | 147 + .../repo/template/HasAspectMethod.java | 78 + .../repo/template/I18NMessageMethod.java | 57 + .../repo/template/NamePathResultsMap.java | 64 + .../template/QNameAwareObjectWrapper.java | 81 + .../repo/template/TemplateServiceImpl.java | 204 + .../template/TemplateServiceImplTest.java | 134 + .../repo/template/XPathResultsMap.java | 48 + .../alfresco/repo/template/test_template1.ftl | 54 + .../AlfrescoTransactionException.java | 39 + .../AlfrescoTransactionSupport.java | 677 ++ .../AlfrescoTransactionSupportTest.java | 158 + .../transaction/DummyTransactionService.java | 62 + .../NodeDaoServiceTransactionInterceptor.java | 63 + .../transaction/TransactionComponent.java | 102 + .../transaction/TransactionComponentTest.java | 134 + .../repo/transaction/TransactionListener.java | 70 + .../repo/transaction/TransactionUtil.java | 166 + .../repo/version/BaseVersionStoreTest.java | 370 + .../repo/version/ContentServiceImplTest.java | 110 + .../repo/version/NodeServiceImpl.java | 562 ++ .../repo/version/NodeServiceImplTest.java | 568 ++ .../repo/version/VersionBootstrap.java | 111 + .../alfresco/repo/version/VersionModel.java | 161 + .../repo/version/VersionServiceImpl.java | 1119 +++ .../repo/version/VersionServiceImplTest.java | 466 ++ .../repo/version/VersionServicePolicies.java | 72 + .../version/VersionStoreBaseTest_model.xml | 112 + .../repo/version/VersionTestSuite.java | 51 + .../repo/version/VersionableAspect.java | 272 + .../common/AbstractVersionServiceImpl.java | 281 + .../version/common/VersionHistoryImpl.java | 195 + .../common/VersionHistoryImplTest.java | 255 + .../repo/version/common/VersionImpl.java | 197 + .../repo/version/common/VersionImplTest.java | 188 + .../repo/version/common/VersionUtil.java | 63 + .../counter/VersionCounterDaoService.java | 53 + .../counter/VersionCounterDaoServiceTest.java | 90 + ...HibernateVersionCounterDaoServiceImpl.java | 107 + .../SerialVersionLabelPolicy.java | 140 + .../SerialVersionLabelPolicyTest.java | 84 + .../alfresco/repo/version/version_model.xml | 156 + .../alfresco/service/ServiceDescriptor.java | 56 + .../alfresco/service/ServiceException.java | 39 + .../org/alfresco/service/ServiceRegistry.java | 223 + .../alfresco/service/cmr/action/Action.java | 203 + .../service/cmr/action/ActionCondition.java | 58 + .../cmr/action/ActionConditionDefinition.java | 29 + .../service/cmr/action/ActionDefinition.java | 29 + .../cmr/action/ActionExecutionStatus.java | 33 + .../service/cmr/action/ActionService.java | 214 + .../cmr/action/ActionServiceException.java | 52 + .../service/cmr/action/CompositeAction.java | 92 + .../cmr/action/ParameterDefinition.java | 60 + .../service/cmr/action/ParameterizedItem.java | 66 + .../action/ParameterizedItemDefinition.java | 67 + .../cmr/coci/CheckOutCheckInService.java | 168 + .../coci/CheckOutCheckInServiceException.java | 53 + .../cmr/dictionary/AspectDefinition.java | 28 + .../cmr/dictionary/AssociationDefinition.java | 113 + .../ChildAssociationDefinition.java | 38 + .../cmr/dictionary/ClassDefinition.java | 105 + .../cmr/dictionary/DataTypeDefinition.java | 86 + .../cmr/dictionary/DictionaryException.java | 39 + .../cmr/dictionary/DictionaryService.java | 163 + .../dictionary/InvalidAspectException.java | 47 + .../cmr/dictionary/InvalidClassException.java | 51 + .../cmr/dictionary/InvalidTypeException.java | 48 + .../cmr/dictionary/ModelDefinition.java | 56 + .../cmr/dictionary/PropertyDefinition.java | 101 + .../cmr/dictionary/TypeDefinition.java | 29 + .../service/cmr/lock/LockService.java | 244 + .../alfresco/service/cmr/lock/LockStatus.java | 30 + .../alfresco/service/cmr/lock/LockType.java | 24 + .../service/cmr/lock/NodeLockedException.java | 56 + .../cmr/lock/UnableToAquireLockException.java | 46 + .../lock/UnableToReleaseLockException.java | 50 + .../cmr/model/FileExistsException.java | 44 + .../service/cmr/model/FileFolderService.java | 206 + .../alfresco/service/cmr/model/FileInfo.java | 72 + .../cmr/model/FileNotFoundException.java | 39 + .../alfresco/service/cmr/model/package.html | 11 + .../repository/AbstractStoreException.java | 47 + .../repository/AspectMissingException.java | 60 + .../AssociationExistsException.java | 62 + .../cmr/repository/AssociationRef.java | 137 + .../cmr/repository/ChildAssociationRef.java | 228 + .../cmr/repository/ContentAccessor.java | 108 + .../service/cmr/repository/ContentData.java | 246 + .../cmr/repository/ContentDataTest.java | 58 + .../cmr/repository/ContentIOException.java | 43 + .../service/cmr/repository/ContentReader.java | 168 + .../cmr/repository/ContentService.java | 124 + .../cmr/repository/ContentStreamListener.java | 33 + .../service/cmr/repository/ContentWriter.java | 142 + .../service/cmr/repository/CopyService.java | 119 + .../cmr/repository/CopyServiceException.java | 48 + .../CyclicChildRelationshipException.java | 42 + .../service/cmr/repository/EntityRef.java | 31 + .../InvalidChildAssociationRefException.java | 49 + .../repository/InvalidNodeRefException.java | 50 + .../repository/InvalidStoreRefException.java | 39 + .../cmr/repository/MimetypeService.java | 86 + .../repository/NoTransformerException.java | 59 + .../service/cmr/repository/NodeRef.java | 161 + .../service/cmr/repository/NodeRefTest.java | 53 + .../service/cmr/repository/NodeService.java | 478 ++ .../alfresco/service/cmr/repository/Path.java | 580 ++ .../service/cmr/repository/PathTest.java | 111 + .../cmr/repository/StoreExistsException.java | 39 + .../service/cmr/repository/StoreRef.java | 111 + .../cmr/repository/TemplateException.java | 61 + .../cmr/repository/TemplateImageResolver.java | 37 + .../service/cmr/repository/TemplateNode.java | 614 ++ .../cmr/repository/TemplateProcessor.java | 38 + .../cmr/repository/TemplateService.java | 67 + .../cmr/repository/XPathException.java | 34 + .../datatype/DefaultTypeConverter.java | 705 ++ .../datatype/DefaultTypeConverterTest.java | 282 + .../cmr/repository/datatype/Duration.java | 1001 +++ .../datatype/TypeConversionException.java | 39 + .../repository/datatype/TypeConverter.java | 596 ++ .../org/alfresco/service/cmr/rule/Rule.java | 56 + .../service/cmr/rule/RuleService.java | 175 + .../cmr/rule/RuleServiceException.java | 53 + .../alfresco/service/cmr/rule/RuleType.java | 58 + .../service/cmr/search/CategoryService.java | 145 + .../search/NamedQueryParameterDefinition.java | 38 + .../service/cmr/search/QueryParameter.java | 55 + .../cmr/search/QueryParameterDefinition.java | 56 + .../service/cmr/search/ResultSet.java | 78 + .../service/cmr/search/ResultSetRow.java | 101 + .../service/cmr/search/SearchParameters.java | 194 + .../service/cmr/search/SearchService.java | 264 + .../service/cmr/search/SearchStatement.java | 61 + .../cmr/security/AccessPermission.java | 57 + .../service/cmr/security/AccessStatus.java | 27 + .../cmr/security/AuthenticationService.java | 146 + .../cmr/security/AuthorityService.java | 175 + .../service/cmr/security/AuthorityType.java | 240 + .../service/cmr/security/OwnableService.java | 58 + .../cmr/security/PermissionService.java | 233 + .../service/cmr/security/PersonService.java | 135 + .../version/ReservedVersionNameException.java | 46 + .../alfresco/service/cmr/version/Version.java | 109 + .../version/VersionDoesNotExistException.java | 39 + .../service/cmr/version/VersionHistory.java | 76 + .../service/cmr/version/VersionService.java | 261 + .../cmr/version/VersionServiceException.java | 49 + .../service/cmr/version/VersionType.java | 27 + .../cmr/view/ExportPackageHandler.java | 60 + .../alfresco/service/cmr/view/Exporter.java | 199 + .../service/cmr/view/ExporterContext.java | 18 + .../cmr/view/ExporterCrawlerParameters.java | 160 + .../service/cmr/view/ExporterException.java | 40 + .../service/cmr/view/ExporterService.java | 64 + .../cmr/view/ImportPackageHandler.java | 55 + .../service/cmr/view/ImporterBinding.java | 7 + .../service/cmr/view/ImporterException.java | 39 + .../service/cmr/view/ImporterProgress.java | 67 + .../service/cmr/view/ImporterService.java | 56 + .../alfresco/service/cmr/view/Location.java | 111 + .../service/descriptor/Descriptor.java | 84 + .../service/descriptor/DescriptorService.java | 42 + .../DynamicNameSpaceResolverTest.java | 104 + .../DynamicNamespacePrefixResolver.java | 137 + .../namespace/InvalidQNameException.java | 33 + .../service/namespace/NamespaceException.java | 33 + .../namespace/NamespacePrefixResolver.java | 63 + .../service/namespace/NamespaceService.java | 96 + .../org/alfresco/service/namespace/QName.java | 476 ++ .../alfresco/service/namespace/QNameMap.java | 175 + .../service/namespace/QNamePattern.java | 41 + .../service/namespace/QNamePatternTest.java | 71 + .../alfresco/service/namespace/QNameTest.java | 262 + .../service/namespace/RegexQNamePattern.java | 119 + .../transaction/TransactionService.java | 69 + source/java/org/alfresco/tools/Export.java | 595 ++ source/java/org/alfresco/tools/Import.java | 419 + source/java/org/alfresco/tools/Tool.java | 209 + .../java/org/alfresco/tools/ToolContext.java | 176 + .../org/alfresco/tools/ToolException.java | 38 + .../util/ApplicationContextHelper.java | 42 + .../alfresco/util/BaseAlfrescoSpringTest.java | 94 + .../alfresco/util/BaseAlfrescoTestCase.java | 101 + .../org/alfresco/util/BaseSpringTest.java | 89 + .../java/org/alfresco/util/PropertyMap.java | 59 + .../util/SearchLanguageConversion.java | 238 + .../util/SearchLanguageConversionTest.java | 103 + .../org/alfresco/util/TestWithUserUtils.java | 111 + .../util/ThreadPoolExecutorFactoryBean.java | 137 + .../util/debug/MethodCallLogAdvice.java | 136 + .../util/debug/NodeStoreInspector.java | 166 + .../debug/OutputSpacesStoreSystemTest.java | 37 + .../util/perf/AbstractPerformanceMonitor.java | 194 + .../util/perf/PerformanceMonitor.java | 119 + .../util/perf/PerformanceMonitorAdvice.java | 70 + .../util/perf/PerformanceMonitorTest.java | 55 + source/java/queryRegister.dtd | 17 + source/test-resources/Plan270904b.xls | Bin 0 -> 2460160 bytes source/test-resources/cache-test-context.xml | 48 + .../farmers_markets_list_2003.doc | Bin 0 -> 30208 bytes .../filefolder/filefolder-test-import.xml | 58 + source/test-resources/quick/quick.bmp | Bin 0 -> 113030 bytes source/test-resources/quick/quick.doc | Bin 0 -> 19968 bytes source/test-resources/quick/quick.gif | Bin 0 -> 10888 bytes source/test-resources/quick/quick.html | 17 + source/test-resources/quick/quick.jpg | Bin 0 -> 26445 bytes source/test-resources/quick/quick.odt | Bin 0 -> 7042 bytes source/test-resources/quick/quick.pdf | Bin 0 -> 18638 bytes source/test-resources/quick/quick.png | Bin 0 -> 28540 bytes source/test-resources/quick/quick.ppt | Bin 0 -> 13824 bytes source/test-resources/quick/quick.sxw | Bin 0 -> 6735 bytes source/test-resources/quick/quick.txt | 1 + source/test-resources/quick/quick.xls | Bin 0 -> 13824 bytes source/test-resources/quick/quick.xml | 5 + source/test-resources/quick/readme.txt | 3 + source/test-resources/testQueryRegister.xml | 67 + source/web/WEB-INF/web.xml | 9 + 1095 files changed, 230566 insertions(+) create mode 100644 .classpath create mode 100644 .externalToolBuilders/JibX.launch create mode 100644 .project create mode 100644 config/alfresco/action-services-context.xml create mode 100644 config/alfresco/application-context.xml create mode 100644 config/alfresco/authentication-services-context.xml create mode 100644 config/alfresco/authority-services-context.xml create mode 100644 config/alfresco/bootstrap/Alfresco-Tutorial.pdf create mode 100644 config/alfresco/bootstrap/categories.xml create mode 100644 config/alfresco/bootstrap/descriptor.xml create mode 100644 config/alfresco/bootstrap/spaces.xml create mode 100644 config/alfresco/bootstrap/tutorial.xml create mode 100644 config/alfresco/cache-context.xml create mode 100644 config/alfresco/content-services-context.xml create mode 100644 config/alfresco/domain/hibernate-cfg.properties create mode 100644 config/alfresco/domain/transaction.properties create mode 100644 config/alfresco/extension/customModel.xml create mode 100644 config/alfresco/extension/exampleModel.xml create mode 100644 config/alfresco/extension/extension-context.xml create mode 100644 config/alfresco/file-servers.xml create mode 100644 config/alfresco/hibernate-context-old.xml create mode 100644 config/alfresco/hibernate-context.xml create mode 100644 config/alfresco/index-recovery-context.xml create mode 100644 config/alfresco/messages/action-config.properties create mode 100644 config/alfresco/messages/action-service.properties create mode 100644 config/alfresco/messages/application-model.properties create mode 100644 config/alfresco/messages/bootstrap-spaces.properties create mode 100644 config/alfresco/messages/bootstrap-templates.properties create mode 100644 config/alfresco/messages/bootstrap-tutorial.properties create mode 100644 config/alfresco/messages/coci-service.properties create mode 100644 config/alfresco/messages/content-model.properties create mode 100644 config/alfresco/messages/content-service.properties create mode 100644 config/alfresco/messages/dictionary-model.properties create mode 100644 config/alfresco/messages/forum-model.properties create mode 100644 config/alfresco/messages/permissions-service.properties create mode 100644 config/alfresco/messages/rule-config.properties create mode 100644 config/alfresco/messages/system-model.properties create mode 100644 config/alfresco/messages/template-service.properties create mode 100644 config/alfresco/messages/version-service.properties create mode 100644 config/alfresco/mimetype-map.xml create mode 100644 config/alfresco/model-specific-services-context.xml create mode 100644 config/alfresco/model/applicationModel.xml create mode 100644 config/alfresco/model/contentModel.xml create mode 100644 config/alfresco/model/dataTypeAnalyzers.properties create mode 100644 config/alfresco/model/dictionaryModel.xml create mode 100644 config/alfresco/model/forumModel.xml create mode 100644 config/alfresco/model/modelSchema.xsd create mode 100644 config/alfresco/model/permissionDefinitions.xml create mode 100644 config/alfresco/model/permissionSchema.dtd create mode 100644 config/alfresco/model/systemModel.xml create mode 100644 config/alfresco/network-protocol-context.xml create mode 100644 config/alfresco/node-services-context.xml create mode 100644 config/alfresco/ownable-services-context-old.xml create mode 100644 config/alfresco/ownable-services-context.xml create mode 100644 config/alfresco/public-services-context.xml create mode 100644 config/alfresco/public-services-security-context-old.xml create mode 100644 config/alfresco/public-services-security-context.xml create mode 100644 config/alfresco/repository.properties create mode 100644 config/alfresco/rule-services-context.xml create mode 100644 config/alfresco/scheduled-jobs-context.xml create mode 100644 config/alfresco/template-services-context.xml create mode 100644 config/alfresco/templates/content/examples/company_logos.ftl create mode 100644 config/alfresco/templates/content/examples/doc_info.ftl create mode 100644 config/alfresco/templates/content/examples/example.ftl create mode 100644 config/alfresco/templates/content/examples/localizable.ftl create mode 100644 config/alfresco/templates/content/examples/my_docs.ftl create mode 100644 config/alfresco/templates/content/examples/my_pressreleases.ftl create mode 100644 config/alfresco/templates/content/examples/my_spaces.ftl create mode 100644 config/alfresco/templates/content/examples/my_summary.ftl create mode 100644 config/alfresco/templates/content/examples/translatable.ftl create mode 100644 config/alfresco/templates/content/examples/userhome_docs.ftl create mode 100644 config/alfresco/templates/content_template_examples.xml create mode 100644 config/alfresco/templates/software_engineering_project.xml create mode 100644 config/alfresco/templates/system-overview.html create mode 100644 config/alfresco/version.properties create mode 100644 config/ehcache.xml create mode 100644 config/treecache.xml create mode 100644 project-build.xml create mode 100644 project-override.properties create mode 100644 project.properties create mode 100644 source/java/org/alfresco/example/SimpleExampleWithContent.java create mode 100644 source/java/org/alfresco/filesys/CIFSServer.java create mode 100644 source/java/org/alfresco/filesys/FTPServer.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPCommand.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPDataSession.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPDate.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPPath.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPRequest.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPSessionList.java create mode 100644 source/java/org/alfresco/filesys/ftp/FTPSrvSession.java create mode 100644 source/java/org/alfresco/filesys/ftp/InvalidPathException.java create mode 100644 source/java/org/alfresco/filesys/locking/FileLock.java create mode 100644 source/java/org/alfresco/filesys/locking/FileLockException.java create mode 100644 source/java/org/alfresco/filesys/locking/FileLockList.java create mode 100644 source/java/org/alfresco/filesys/locking/FileUnlockException.java create mode 100644 source/java/org/alfresco/filesys/locking/LockConflictException.java create mode 100644 source/java/org/alfresco/filesys/locking/NotLockedException.java create mode 100644 source/java/org/alfresco/filesys/netbios/NameTemplateException.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSDatagram.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSDatagramSocket.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSException.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSName.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSNameList.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSPacket.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetBIOSSession.java create mode 100644 source/java/org/alfresco/filesys/netbios/NetworkSettings.java create mode 100644 source/java/org/alfresco/filesys/netbios/RFCNetBIOSProtocol.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/AddNameListener.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/NetBIOSNameEvent.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/NetBIOSNameServer.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/NetBIOSRequest.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/PacketReceiver.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/QueryNameListener.java create mode 100644 source/java/org/alfresco/filesys/netbios/server/RemoteNameListener.java create mode 100644 source/java/org/alfresco/filesys/netbios/win32/NetBIOS.java create mode 100644 source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocket.java create mode 100644 source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocketException.java create mode 100644 source/java/org/alfresco/filesys/netbios/win32/Win32NetBIOS.java create mode 100644 source/java/org/alfresco/filesys/netbios/win32/WinsockError.java create mode 100644 source/java/org/alfresco/filesys/netbios/win32/WinsockNetBIOSException.java create mode 100644 source/java/org/alfresco/filesys/server/NetworkServer.java create mode 100644 source/java/org/alfresco/filesys/server/NetworkServerList.java create mode 100644 source/java/org/alfresco/filesys/server/ServerListener.java create mode 100644 source/java/org/alfresco/filesys/server/SessionListener.java create mode 100644 source/java/org/alfresco/filesys/server/SrvSession.java create mode 100644 source/java/org/alfresco/filesys/server/SrvSessionList.java create mode 100644 source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java create mode 100644 source/java/org/alfresco/filesys/server/auth/ClientInfo.java create mode 100644 source/java/org/alfresco/filesys/server/auth/DefaultAuthenticator.java create mode 100644 source/java/org/alfresco/filesys/server/auth/InvalidUserException.java create mode 100644 source/java/org/alfresco/filesys/server/auth/LocalAuthenticator.java create mode 100644 source/java/org/alfresco/filesys/server/auth/PasswordEncryptor.java create mode 100644 source/java/org/alfresco/filesys/server/auth/SrvAuthenticator.java create mode 100644 source/java/org/alfresco/filesys/server/auth/UserAccount.java create mode 100644 source/java/org/alfresco/filesys/server/auth/UserAccountList.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/ACLParseException.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/AccessControl.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/AccessControlFactory.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/AccessControlList.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/AccessControlManager.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/AccessControlParser.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/DefaultAccessControlManager.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControl.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControlParser.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/InvalidACLTypeException.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControl.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControlParser.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControl.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControlParser.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/UserAccessControl.java create mode 100644 source/java/org/alfresco/filesys/server/auth/acl/UserAccessControlParser.java create mode 100644 source/java/org/alfresco/filesys/server/config/IncompleteConfigurationException.java create mode 100644 source/java/org/alfresco/filesys/server/config/InvalidConfigurationException.java create mode 100644 source/java/org/alfresco/filesys/server/config/ServerConfiguration.java create mode 100644 source/java/org/alfresco/filesys/server/core/DeviceContext.java create mode 100644 source/java/org/alfresco/filesys/server/core/DeviceContextException.java create mode 100644 source/java/org/alfresco/filesys/server/core/DeviceInterface.java create mode 100644 source/java/org/alfresco/filesys/server/core/InvalidDeviceInterfaceException.java create mode 100644 source/java/org/alfresco/filesys/server/core/ShareMapper.java create mode 100644 source/java/org/alfresco/filesys/server/core/ShareType.java create mode 100644 source/java/org/alfresco/filesys/server/core/SharedDevice.java create mode 100644 source/java/org/alfresco/filesys/server/core/SharedDeviceList.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/AccessDeniedException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/AccessMode.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DefaultShareMapper.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DeviceAttribute.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DirectoryNotEmptyException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskDeviceContext.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskFullException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskInfo.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskInterface.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskSharedDevice.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskSizeInterface.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/DiskVolumeInterface.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileAccess.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileAction.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileAttribute.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileExistsException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileIdInterface.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileInfo.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileName.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileOfflineException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileOpenParams.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileSharingException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileStatus.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/FileSystem.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/HomeShareMapper.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/MediaOfflineException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/NetworkFile.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/NetworkFileServer.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/NotifyChange.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/PathNotFoundException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/SearchContext.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/ShareListener.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/SrvDiskInfo.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/TooManyConnectionsException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/TooManyFilesException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/TreeConnection.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/TreeConnectionHash.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/UnsupportedInfoLevelException.java create mode 100644 source/java/org/alfresco/filesys/server/filesys/VolumeInfo.java create mode 100644 source/java/org/alfresco/filesys/server/locking/FileLockListener.java create mode 100644 source/java/org/alfresco/filesys/server/locking/FileLockingInterface.java create mode 100644 source/java/org/alfresco/filesys/server/locking/LockManager.java create mode 100644 source/java/org/alfresco/filesys/smb/Capability.java create mode 100644 source/java/org/alfresco/filesys/smb/DataType.java create mode 100644 source/java/org/alfresco/filesys/smb/Dialect.java create mode 100644 source/java/org/alfresco/filesys/smb/DialectSelector.java create mode 100644 source/java/org/alfresco/filesys/smb/DirectoryWatcher.java create mode 100644 source/java/org/alfresco/filesys/smb/FileInfoLevel.java create mode 100644 source/java/org/alfresco/filesys/smb/FindFirstNext.java create mode 100644 source/java/org/alfresco/filesys/smb/InvalidUNCPathException.java create mode 100644 source/java/org/alfresco/filesys/smb/LockingAndX.java create mode 100644 source/java/org/alfresco/filesys/smb/NTIOCtl.java create mode 100644 source/java/org/alfresco/filesys/smb/NTTime.java create mode 100644 source/java/org/alfresco/filesys/smb/NetworkSession.java create mode 100644 source/java/org/alfresco/filesys/smb/PCShare.java create mode 100644 source/java/org/alfresco/filesys/smb/PacketType.java create mode 100644 source/java/org/alfresco/filesys/smb/Protocol.java create mode 100644 source/java/org/alfresco/filesys/smb/SMBDate.java create mode 100644 source/java/org/alfresco/filesys/smb/SMBDeviceType.java create mode 100644 source/java/org/alfresco/filesys/smb/SMBErrorText.java create mode 100644 source/java/org/alfresco/filesys/smb/SMBException.java create mode 100644 source/java/org/alfresco/filesys/smb/SMBStatus.java create mode 100644 source/java/org/alfresco/filesys/smb/SeekType.java create mode 100644 source/java/org/alfresco/filesys/smb/ServerType.java create mode 100644 source/java/org/alfresco/filesys/smb/SharingMode.java create mode 100644 source/java/org/alfresco/filesys/smb/TcpipSMB.java create mode 100644 source/java/org/alfresco/filesys/smb/TransactBuffer.java create mode 100644 source/java/org/alfresco/filesys/smb/TransactionNames.java create mode 100644 source/java/org/alfresco/filesys/smb/WinNT.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEBuffer.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEBufferException.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCECommand.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEDataPacker.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEException.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEList.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEPipeType.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEReadable.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEReadableList.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteable.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteableList.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandle.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandleCache.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/Srvsvc.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/UUID.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/Wkssvc.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfo.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfoList.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/ServerInfo.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfo.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfoList.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/UserInfo.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/info/WorkstationInfo.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/server/DCEHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeFile.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/server/DCESrvPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/server/SrvsvcDCEHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/dcerpc/server/WkssvcDCEHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/mailslot/HostAnnouncer.java create mode 100644 source/java/org/alfresco/filesys/smb/mailslot/MailSlot.java create mode 100644 source/java/org/alfresco/filesys/smb/mailslot/SMBMailslotPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/mailslot/TcpipNetBIOSHostAnnouncer.java create mode 100644 source/java/org/alfresco/filesys/smb/mailslot/Win32NetBIOSHostAnnouncer.java create mode 100644 source/java/org/alfresco/filesys/smb/mailslot/WinsockNetBIOSHostAnnouncer.java create mode 100644 source/java/org/alfresco/filesys/smb/server/AdminSharedDevice.java create mode 100644 source/java/org/alfresco/filesys/smb/server/CoreProtocolHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/CoreResumeKey.java create mode 100644 source/java/org/alfresco/filesys/smb/server/DCERPCHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/DiskInfoPacker.java create mode 100644 source/java/org/alfresco/filesys/smb/server/Find.java create mode 100644 source/java/org/alfresco/filesys/smb/server/FindInfoPacker.java create mode 100644 source/java/org/alfresco/filesys/smb/server/IPCHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/LanManProtocolHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/NTParameterPacker.java create mode 100644 source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/NTTransPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/server/NamedPipeTransaction.java create mode 100644 source/java/org/alfresco/filesys/smb/server/NetBIOSPacketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/NetBIOSSessionSocketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/OpenAndX.java create mode 100644 source/java/org/alfresco/filesys/smb/server/PacketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/PipeDevice.java create mode 100644 source/java/org/alfresco/filesys/smb/server/PipeLanmanHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/ProtocolFactory.java create mode 100644 source/java/org/alfresco/filesys/smb/server/ProtocolHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/QueryInfoPacker.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBServer.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBSrvException.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBSrvPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBSrvSession.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBSrvSessionState.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBSrvTransPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SMBTransPacket.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SessionSocketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SrvSessionFactory.java create mode 100644 source/java/org/alfresco/filesys/smb/server/SrvTransactBuffer.java create mode 100644 source/java/org/alfresco/filesys/smb/server/TcpipSMBPacketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/TcpipSMBSessionSocketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEvent.java create mode 100644 source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEventList.java create mode 100644 source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/notify/NotifyRequest.java create mode 100644 source/java/org/alfresco/filesys/smb/server/notify/NotifyRequestList.java create mode 100644 source/java/org/alfresco/filesys/smb/server/ntfs/NTFSStreamsInterface.java create mode 100644 source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfo.java create mode 100644 source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfoList.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/CifsIntegrationTest.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/ContentContext.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/ContentDiskInterface.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/ContentSearchContext.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/FileState.java create mode 100644 source/java/org/alfresco/filesys/smb/server/repo/FileStateTable.java create mode 100644 source/java/org/alfresco/filesys/smb/server/win32/LanaListener.java create mode 100644 source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSLanaMonitor.java create mode 100644 source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSPacketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSSessionSocketHandler.java create mode 100644 source/java/org/alfresco/filesys/smb/server/win32/WinsockNetBIOSPacketHandler.java create mode 100644 source/java/org/alfresco/filesys/util/DataBuffer.java create mode 100644 source/java/org/alfresco/filesys/util/DataPacker.java create mode 100644 source/java/org/alfresco/filesys/util/HexDump.java create mode 100644 source/java/org/alfresco/filesys/util/IPAddress.java create mode 100644 source/java/org/alfresco/filesys/util/MemorySize.java create mode 100644 source/java/org/alfresco/filesys/util/StringList.java create mode 100644 source/java/org/alfresco/filesys/util/WildCard.java create mode 100644 source/java/org/alfresco/model/ContentModel.java create mode 100644 source/java/org/alfresco/model/ForumModel.java create mode 100644 source/java/org/alfresco/repo/action/ActionConditionDefinitionImpl.java create mode 100644 source/java/org/alfresco/repo/action/ActionConditionDefinitionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ActionConditionImpl.java create mode 100644 source/java/org/alfresco/repo/action/ActionConditionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ActionDefinitionImpl.java create mode 100644 source/java/org/alfresco/repo/action/ActionDefinitionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ActionImpl.java create mode 100644 source/java/org/alfresco/repo/action/ActionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ActionModel.java create mode 100644 source/java/org/alfresco/repo/action/ActionServiceImpl.java create mode 100644 source/java/org/alfresco/repo/action/ActionServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ActionTestSuite.java create mode 100644 source/java/org/alfresco/repo/action/ActionTransactionListener.java create mode 100644 source/java/org/alfresco/repo/action/ActionsAspect.java create mode 100644 source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueue.java create mode 100644 source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueueImpl.java create mode 100644 source/java/org/alfresco/repo/action/BaseParameterizedItemDefinitionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/BaseParameterizedItemImplTest.java create mode 100644 source/java/org/alfresco/repo/action/CommonResourceAbstractBase.java create mode 100644 source/java/org/alfresco/repo/action/CompositeActionImpl.java create mode 100644 source/java/org/alfresco/repo/action/CompositeActionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ParameterDefinitionImpl.java create mode 100644 source/java/org/alfresco/repo/action/ParameterDefinitionImplTest.java create mode 100644 source/java/org/alfresco/repo/action/ParameterizedItemAbstractBase.java create mode 100644 source/java/org/alfresco/repo/action/ParameterizedItemDefinitionImpl.java create mode 100644 source/java/org/alfresco/repo/action/ParameterizedItemImpl.java create mode 100644 source/java/org/alfresco/repo/action/RuntimeActionService.java create mode 100644 source/java/org/alfresco/repo/action/actionModel.xml create mode 100644 source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluatorAbstractBase.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluatorTest.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluatorTest.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluatorTest.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/HasVersionHistoryEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/InCategoryEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluatorTest.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/NoConditionEvaluator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/compare/ComparePropertyValueOperation.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/compare/ContentPropertyName.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/compare/DatePropertyValueComparator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/compare/NumericPropertyValueComparator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/compare/PropertyValueComparator.java create mode 100644 source/java/org/alfresco/repo/action/evaluator/compare/TextPropertyValueComparator.java create mode 100644 source/java/org/alfresco/repo/action/executer/ActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/ActionExecuterAbstractBase.java create mode 100644 source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuterTest.java create mode 100644 source/java/org/alfresco/repo/action/executer/CheckInActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/CheckOutActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/CompositeActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTest.java create mode 100644 source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/CreateVersionActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/ExporterActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/ImporterActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/LinkCategoryActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/MailActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/MoveActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuterTest.java create mode 100644 source/java/org/alfresco/repo/action/executer/SimpleWorkflowActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuter.java create mode 100644 source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuterTest.java create mode 100644 source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java create mode 100644 source/java/org/alfresco/repo/audit/AuditableAspect.java create mode 100644 source/java/org/alfresco/repo/audit/AuditableAspectTest.java create mode 100644 source/java/org/alfresco/repo/cache/CacheTest.java create mode 100644 source/java/org/alfresco/repo/cache/EhCacheAdapter.java create mode 100644 source/java/org/alfresco/repo/cache/SimpleCache.java create mode 100644 source/java/org/alfresco/repo/cache/TransactionalCache.java create mode 100644 source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java create mode 100644 source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/coci/WorkingCopyAspect.java create mode 100644 source/java/org/alfresco/repo/configuration/ConfigurableService.java create mode 100644 source/java/org/alfresco/repo/configuration/ConfigurableServiceImpl.java create mode 100644 source/java/org/alfresco/repo/configuration/ConfigurableServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/content/AbstractContentAccessor.java create mode 100644 source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java create mode 100644 source/java/org/alfresco/repo/content/AbstractContentReader.java create mode 100644 source/java/org/alfresco/repo/content/AbstractContentStore.java create mode 100644 source/java/org/alfresco/repo/content/AbstractContentWriter.java create mode 100644 source/java/org/alfresco/repo/content/ContentServicePolicies.java create mode 100644 source/java/org/alfresco/repo/content/ContentStore.java create mode 100644 source/java/org/alfresco/repo/content/ContentStoreCleanupJob.java create mode 100644 source/java/org/alfresco/repo/content/ContentStoreCleanupJobTest.java create mode 100644 source/java/org/alfresco/repo/content/MimetypeMap.java create mode 100644 source/java/org/alfresco/repo/content/MimetypeMapTest.java create mode 100644 source/java/org/alfresco/repo/content/RandomAccessContent.java create mode 100644 source/java/org/alfresco/repo/content/RoutingContentService.java create mode 100644 source/java/org/alfresco/repo/content/RoutingContentServiceTest.java create mode 100644 source/java/org/alfresco/repo/content/filestore/FileContentReader.java create mode 100644 source/java/org/alfresco/repo/content/filestore/FileContentStore.java create mode 100644 source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java create mode 100644 source/java/org/alfresco/repo/content/filestore/FileContentWriter.java create mode 100644 source/java/org/alfresco/repo/content/filestore/FileIOTest.java create mode 100644 source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracterTest.java create mode 100644 source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracterTest.java create mode 100644 source/java/org/alfresco/repo/content/metadata/MP3MetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/MetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/MetadataExtracterRegistry.java create mode 100644 source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracterTest.java create mode 100644 source/java/org/alfresco/repo/content/metadata/OpenDocumentMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracterTest.java create mode 100644 source/java/org/alfresco/repo/content/metadata/StringMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracter.java create mode 100644 source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracterTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/AbstractContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/AbstractContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/ComplexContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/CompoundContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/ContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java create mode 100644 source/java/org/alfresco/repo/content/transform/ContentTransformerRegistryTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/TextMiningContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/TextMiningContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/UnoContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/UnoContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/magick/AbstractImageMagickContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformer.java create mode 100644 source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformerTest.java create mode 100644 source/java/org/alfresco/repo/content/transform/magick/alfresco.gif create mode 100644 source/java/org/alfresco/repo/copy/CopyServiceImpl.java create mode 100644 source/java/org/alfresco/repo/copy/CopyServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/copy/CopyServicePolicies.java create mode 100644 source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java create mode 100644 source/java/org/alfresco/repo/descriptor/DescriptorServiceTest.java create mode 100644 source/java/org/alfresco/repo/dictionary/CompiledModel.java create mode 100644 source/java/org/alfresco/repo/dictionary/DelegateModelQuery.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryBootstrap.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryComponent.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryDAO.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryDAOImpl.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryModelType.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryNamespaceComponent.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrap.java create mode 100644 source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrapTest.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2AnonymousTypeDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Aspect.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2AspectDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Association.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2ChildAssociation.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2ChildAssociationDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Class.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2ClassDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2DataType.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2DataTypeDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Label.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Model.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2ModelDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Namespace.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Property.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2PropertyDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2PropertyOverride.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2Type.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2TypeDefinition.java create mode 100644 source/java/org/alfresco/repo/dictionary/M2XML.java create mode 100644 source/java/org/alfresco/repo/dictionary/ModelQuery.java create mode 100644 source/java/org/alfresco/repo/dictionary/NamespaceDAO.java create mode 100644 source/java/org/alfresco/repo/dictionary/NamespaceDAOImpl.java create mode 100644 source/java/org/alfresco/repo/dictionary/TestModel.java create mode 100644 source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.properties create mode 100644 source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml create mode 100644 source/java/org/alfresco/repo/dictionary/m2binding.xml create mode 100644 source/java/org/alfresco/repo/domain/ChildAssoc.java create mode 100644 source/java/org/alfresco/repo/domain/Node.java create mode 100644 source/java/org/alfresco/repo/domain/NodeAssoc.java create mode 100644 source/java/org/alfresco/repo/domain/NodeKey.java create mode 100644 source/java/org/alfresco/repo/domain/NodeStatus.java create mode 100644 source/java/org/alfresco/repo/domain/PropertyValue.java create mode 100644 source/java/org/alfresco/repo/domain/Store.java create mode 100644 source/java/org/alfresco/repo/domain/StoreKey.java create mode 100644 source/java/org/alfresco/repo/domain/VersionCount.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/ChildAssocImpl.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml create mode 100644 source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/NodeStatusImpl.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/QNameUserType.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/Store.hbm.xml create mode 100644 source/java/org/alfresco/repo/domain/hibernate/StoreImpl.java create mode 100644 source/java/org/alfresco/repo/domain/hibernate/VersionCount.hbm.xml create mode 100644 source/java/org/alfresco/repo/domain/hibernate/VersionCountImpl.java create mode 100644 source/java/org/alfresco/repo/exporter/ACPExportPackageHandler.java create mode 100644 source/java/org/alfresco/repo/exporter/ChainedExporter.java create mode 100644 source/java/org/alfresco/repo/exporter/ExporterComponent.java create mode 100644 source/java/org/alfresco/repo/exporter/ExporterComponentTest.java create mode 100644 source/java/org/alfresco/repo/exporter/ExporterCrawler.java create mode 100644 source/java/org/alfresco/repo/exporter/FileExportPackageHandler.java create mode 100644 source/java/org/alfresco/repo/exporter/URLExporter.java create mode 100644 source/java/org/alfresco/repo/exporter/ViewXMLExporter.java create mode 100644 source/java/org/alfresco/repo/importer/ACPImportPackageHandler.java create mode 100644 source/java/org/alfresco/repo/importer/DefaultContentHandler.java create mode 100644 source/java/org/alfresco/repo/importer/FileImportPackageHandler.java create mode 100644 source/java/org/alfresco/repo/importer/FileImporter.java create mode 100644 source/java/org/alfresco/repo/importer/FileImporterException.java create mode 100644 source/java/org/alfresco/repo/importer/FileImporterImpl.java create mode 100644 source/java/org/alfresco/repo/importer/FileImporterTest.java create mode 100644 source/java/org/alfresco/repo/importer/ImportContentHandler.java create mode 100644 source/java/org/alfresco/repo/importer/ImportNode.java create mode 100644 source/java/org/alfresco/repo/importer/ImportParent.java create mode 100644 source/java/org/alfresco/repo/importer/Importer.java create mode 100644 source/java/org/alfresco/repo/importer/ImporterBootstrap.java create mode 100644 source/java/org/alfresco/repo/importer/ImporterComponent.java create mode 100644 source/java/org/alfresco/repo/importer/ImporterComponentTest.java create mode 100644 source/java/org/alfresco/repo/importer/Parser.java create mode 100644 source/java/org/alfresco/repo/importer/importercomponent_test.xml create mode 100644 source/java/org/alfresco/repo/importer/importercomponent_testfile.txt create mode 100644 source/java/org/alfresco/repo/importer/view/ElementContext.java create mode 100644 source/java/org/alfresco/repo/importer/view/MetaDataContext.java create mode 100644 source/java/org/alfresco/repo/importer/view/NodeContext.java create mode 100644 source/java/org/alfresco/repo/importer/view/NodeItemContext.java create mode 100644 source/java/org/alfresco/repo/importer/view/ParentContext.java create mode 100644 source/java/org/alfresco/repo/importer/view/ViewParser.java create mode 100644 source/java/org/alfresco/repo/lock/LockBehaviourImplTest.java create mode 100644 source/java/org/alfresco/repo/lock/LockServiceImpl.java create mode 100644 source/java/org/alfresco/repo/lock/LockServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/lock/LockTestSuite.java create mode 100644 source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java create mode 100644 source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java create mode 100644 source/java/org/alfresco/repo/model/package.html create mode 100644 source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java create mode 100644 source/java/org/alfresco/repo/node/BaseNodeServiceTest.java create mode 100644 source/java/org/alfresco/repo/node/BaseNodeServiceTest_model.xml create mode 100644 source/java/org/alfresco/repo/node/ConcurrentNodeServiceTest.java create mode 100644 source/java/org/alfresco/repo/node/NodeServicePolicies.java create mode 100644 source/java/org/alfresco/repo/node/PerformanceNodeServiceTest.java create mode 100644 source/java/org/alfresco/repo/node/ReferenceableAspect.java create mode 100644 source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java create mode 100644 source/java/org/alfresco/repo/node/db/DbNodeServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/node/db/NodeDaoService.java create mode 100644 source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java create mode 100644 source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponent.java create mode 100644 source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponentTest.java create mode 100644 source/java/org/alfresco/repo/node/index/IndexRecovery.java create mode 100644 source/java/org/alfresco/repo/node/index/IndexRecoveryJob.java create mode 100644 source/java/org/alfresco/repo/node/index/NodeIndexer.java create mode 100644 source/java/org/alfresco/repo/node/index/NodeIndexerTest.java create mode 100644 source/java/org/alfresco/repo/node/integrity/AbstractIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/AssocSourceMultiplicityIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/AssocSourceTypeIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/AssocTargetMultiplicityIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/AssocTargetTypeIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityChecker.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityEventTest.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityException.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityRecord.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityTest.java create mode 100644 source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml create mode 100644 source/java/org/alfresco/repo/node/integrity/PropertiesIntegrityEvent.java create mode 100644 source/java/org/alfresco/repo/ownable/impl/OwnableServiceImpl.java create mode 100644 source/java/org/alfresco/repo/ownable/impl/OwnableServiceNOOPImpl.java create mode 100644 source/java/org/alfresco/repo/ownable/impl/OwnableServiceTest.java create mode 100644 source/java/org/alfresco/repo/policy/AssociationPolicy.java create mode 100644 source/java/org/alfresco/repo/policy/AssociationPolicyDelegate.java create mode 100644 source/java/org/alfresco/repo/policy/Behaviour.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourBinding.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourChangeObserver.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourDefinition.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourFilter.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourFilterImpl.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourIndex.java create mode 100644 source/java/org/alfresco/repo/policy/BehaviourMap.java create mode 100644 source/java/org/alfresco/repo/policy/CachedPolicyFactory.java create mode 100644 source/java/org/alfresco/repo/policy/ClassBehaviourBinding.java create mode 100644 source/java/org/alfresco/repo/policy/ClassBehaviourIndex.java create mode 100644 source/java/org/alfresco/repo/policy/ClassFeatureBehaviourBinding.java create mode 100644 source/java/org/alfresco/repo/policy/ClassPolicy.java create mode 100644 source/java/org/alfresco/repo/policy/ClassPolicyDelegate.java create mode 100644 source/java/org/alfresco/repo/policy/JavaBehaviour.java create mode 100644 source/java/org/alfresco/repo/policy/Policy.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyComponent.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyComponentImpl.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyComponentTest.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyDefinition.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyException.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyFactory.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyList.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyScope.java create mode 100644 source/java/org/alfresco/repo/policy/PolicyType.java create mode 100644 source/java/org/alfresco/repo/policy/PropertyPolicy.java create mode 100644 source/java/org/alfresco/repo/policy/PropertyPolicyDelegate.java create mode 100644 source/java/org/alfresco/repo/policy/ServiceBehaviourBinding.java create mode 100644 source/java/org/alfresco/repo/policy/policycomponenttest_model.xml create mode 100644 source/java/org/alfresco/repo/rule/BaseRuleTest.java create mode 100644 source/java/org/alfresco/repo/rule/RuleCache.java create mode 100644 source/java/org/alfresco/repo/rule/RuleImpl.java create mode 100644 source/java/org/alfresco/repo/rule/RuleModel.java create mode 100644 source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java create mode 100644 source/java/org/alfresco/repo/rule/RuleServiceImpl.java create mode 100644 source/java/org/alfresco/repo/rule/RuleServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/rule/RuleTestSuite.java create mode 100644 source/java/org/alfresco/repo/rule/RuleTransactionListener.java create mode 100644 source/java/org/alfresco/repo/rule/RuleTypeImpl.java create mode 100644 source/java/org/alfresco/repo/rule/RuleTypeImplTest.java create mode 100644 source/java/org/alfresco/repo/rule/RulesAspect.java create mode 100644 source/java/org/alfresco/repo/rule/RuntimeRuleService.java create mode 100644 source/java/org/alfresco/repo/rule/ruleModel.xml create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/RuleTrigger.java create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerAbstractBase.java create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/SingleAssocRefPolicyRuleTrigger.java create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/SingleChildAssocRefPolicyRuleTrigger.java create mode 100644 source/java/org/alfresco/repo/rule/ruletrigger/SingleNodeRefPolicyRuleTrigger.java create mode 100644 source/java/org/alfresco/repo/search/AbstractResultSet.java create mode 100644 source/java/org/alfresco/repo/search/AbstractResultSetRow.java create mode 100644 source/java/org/alfresco/repo/search/AbstractResultSetRowIterator.java create mode 100644 source/java/org/alfresco/repo/search/AbstractSearcherComponent.java create mode 100644 source/java/org/alfresco/repo/search/CannedQueryDef.java create mode 100644 source/java/org/alfresco/repo/search/CannedQueryDefImpl.java create mode 100644 source/java/org/alfresco/repo/search/DocumentNavigator.java create mode 100644 source/java/org/alfresco/repo/search/EmptyResultSet.java create mode 100644 source/java/org/alfresco/repo/search/ISO9075.java create mode 100644 source/java/org/alfresco/repo/search/ISO9075Test.java create mode 100644 source/java/org/alfresco/repo/search/Indexer.java create mode 100644 source/java/org/alfresco/repo/search/IndexerAndSearcher.java create mode 100644 source/java/org/alfresco/repo/search/IndexerAndSearcherFactoryException.java create mode 100644 source/java/org/alfresco/repo/search/IndexerComponent.java create mode 100644 source/java/org/alfresco/repo/search/IndexerException.java create mode 100644 source/java/org/alfresco/repo/search/NodeServiceXPath.java create mode 100644 source/java/org/alfresco/repo/search/QueryCollection.java create mode 100644 source/java/org/alfresco/repo/search/QueryCollectionImpl.java create mode 100644 source/java/org/alfresco/repo/search/QueryParameterDefImpl.java create mode 100644 source/java/org/alfresco/repo/search/QueryParameterRefImpl.java create mode 100644 source/java/org/alfresco/repo/search/QueryRegisterComponent.java create mode 100644 source/java/org/alfresco/repo/search/QueryRegisterComponentImpl.java create mode 100644 source/java/org/alfresco/repo/search/QueryRegisterComponentTest.java create mode 100644 source/java/org/alfresco/repo/search/ResultSetRowIterator.java create mode 100644 source/java/org/alfresco/repo/search/SearcherComponent.java create mode 100644 source/java/org/alfresco/repo/search/SearcherComponentTest.java create mode 100644 source/java/org/alfresco/repo/search/SearcherException.java create mode 100644 source/java/org/alfresco/repo/search/impl/JCR170Searcher.java create mode 100644 source/java/org/alfresco/repo/search/impl/NoActionIndexer.java create mode 100644 source/java/org/alfresco/repo/search/impl/NodeSearcher.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/CharStream.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/ClosingIndexSearcher.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/DebugXPathHandler.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/FastCharStream.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/FilterIndexReaderByNodeRefs.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/Lockable.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneBase.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryServiceImpl.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryTest.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneConfig.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexBackupComponentTest.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexException.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexer.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcher.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcherFactory.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerImpl.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneQueryParser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSet.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRow.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRowIterator.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcher.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneTest.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneTest_model.xml create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/LuceneXPathHandler.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/ParseException.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/QueryParameterisationException.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/QueryParser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/QueryParser.jj create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/QueryParserConstants.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/QueryParserTokenManager.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/Token.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/TokenMgrError.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/CategoryAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/DateAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/DateTokenFilter.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleTokenFilter.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatTokenFilter.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerTokenFilter.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/LongAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/LongTokenFilter.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncoder.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncodingTest.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/PathAnalyser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokenFilter.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokeniser.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerAware.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerException.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerJob.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexer.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexerImpl.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/AbsoluteStructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/AbstractStructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/AnyStructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/CachingTermPositions.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/ContainerScorer.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/DeltaReader.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/DescendantAndSelfStructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/LeafScorer.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/PathQuery.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/PathScorer.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/RelativeStructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/SelfAxisStructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldPosition.java create mode 100644 source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldTerm.java create mode 100644 source/java/org/alfresco/repo/search/results/ChildAssocRefResultSet.java create mode 100644 source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRow.java create mode 100644 source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRowIterator.java create mode 100644 source/java/org/alfresco/repo/search/results/DetachedResultSet.java create mode 100644 source/java/org/alfresco/repo/search/results/DetachedResultSetRow.java create mode 100644 source/java/org/alfresco/repo/search/transaction/LuceneIndexLock.java create mode 100644 source/java/org/alfresco/repo/search/transaction/LuceneTransactionException.java create mode 100644 source/java/org/alfresco/repo/search/transaction/SimpleTransaction.java create mode 100644 source/java/org/alfresco/repo/search/transaction/SimpleTransactionManager.java create mode 100644 source/java/org/alfresco/repo/search/transaction/XidException.java create mode 100644 source/java/org/alfresco/repo/search/transaction/XidTransaction.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AuthenticatedAuthenticationPassthroughProvider.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AuthenticationComponent.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AuthenticationException.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java create mode 100644 source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java create mode 100644 source/java/org/alfresco/repo/security/authentication/DefaultMutableAuthenticationDao.java create mode 100644 source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java create mode 100644 source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoder.java create mode 100644 source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoderImpl.java create mode 100644 source/java/org/alfresco/repo/security/authentication/MutableAuthenticationDao.java create mode 100644 source/java/org/alfresco/repo/security/authentication/NTLMMode.java create mode 100644 source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java create mode 100644 source/java/org/alfresco/repo/security/authentication/TicketComponent.java create mode 100644 source/java/org/alfresco/repo/security/authentication/TicketExpiredException.java create mode 100644 source/java/org/alfresco/repo/security/authentication/userModel.xml create mode 100644 source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceImpl.java create mode 100644 source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/AccessDeniedException.java create mode 100644 source/java/org/alfresco/repo/security/permissions/AuthorityReference.java create mode 100644 source/java/org/alfresco/repo/security/permissions/DynamicAuthority.java create mode 100644 source/java/org/alfresco/repo/security/permissions/NodePermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/PermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/PermissionReference.java create mode 100644 source/java/org/alfresco/repo/security/permissions/PermissionServiceSPI.java create mode 100644 source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java create mode 100644 source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthorityTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/dynamic/OwnerDynamicAuthority.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/AbstractNodePermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionReference.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/AlwaysProceedMethodInterceptor.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/ExceptionTranslatorMethodInterceptor.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/ModelDAO.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/PermissionReferenceImpl.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/PermissionsDAO.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/RequiredPermission.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/SimpleNodePermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionReference.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterException.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSet.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSetTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityInterceptor.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionsDAO.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntryImpl.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/Permission.hbm.xml create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntryImpl.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReference.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReferenceImpl.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/Recipient.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/hibernate/RecipientImpl.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/AbstractPermission.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/DynamicPermission.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/GlobalPermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/ModelPermissionEntry.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/NodePermission.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/Permission.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/PermissionGroup.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelException.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/PermissionSet.java create mode 100644 source/java/org/alfresco/repo/security/permissions/impl/model/XMLModelInitialisable.java create mode 100644 source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java create mode 100644 source/java/org/alfresco/repo/security/person/PersonException.java create mode 100644 source/java/org/alfresco/repo/security/person/PersonServiceImpl.java create mode 100644 source/java/org/alfresco/repo/security/person/PersonTest.java create mode 100644 source/java/org/alfresco/repo/service/BeanServiceDescriptor.java create mode 100644 source/java/org/alfresco/repo/service/ServiceDescriptorAdvisor.java create mode 100644 source/java/org/alfresco/repo/service/ServiceDescriptorAdvisorFactory.java create mode 100644 source/java/org/alfresco/repo/service/ServiceDescriptorMetaData.java create mode 100644 source/java/org/alfresco/repo/service/ServiceDescriptorMixin.java create mode 100644 source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java create mode 100644 source/java/org/alfresco/repo/service/ServiceDescriptorRegistryTest.java create mode 100644 source/java/org/alfresco/repo/service/StoreRedirector.java create mode 100644 source/java/org/alfresco/repo/service/StoreRedirectorProxyFactory.java create mode 100644 source/java/org/alfresco/repo/service/StoreRedirectorProxyFactoryTest.java create mode 100644 source/java/org/alfresco/repo/service/serviceregistrytest_model.xml create mode 100644 source/java/org/alfresco/repo/service/testredirector.xml create mode 100644 source/java/org/alfresco/repo/service/testregistry.xml create mode 100644 source/java/org/alfresco/repo/template/BasePathResultsMap.java create mode 100644 source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java create mode 100644 source/java/org/alfresco/repo/template/ClassPathTemplateLoader.java create mode 100644 source/java/org/alfresco/repo/template/DateCompareMethod.java create mode 100644 source/java/org/alfresco/repo/template/FreeMarkerProcessor.java create mode 100644 source/java/org/alfresco/repo/template/HasAspectMethod.java create mode 100644 source/java/org/alfresco/repo/template/I18NMessageMethod.java create mode 100644 source/java/org/alfresco/repo/template/NamePathResultsMap.java create mode 100644 source/java/org/alfresco/repo/template/QNameAwareObjectWrapper.java create mode 100644 source/java/org/alfresco/repo/template/TemplateServiceImpl.java create mode 100644 source/java/org/alfresco/repo/template/TemplateServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/template/XPathResultsMap.java create mode 100644 source/java/org/alfresco/repo/template/test_template1.ftl create mode 100644 source/java/org/alfresco/repo/transaction/AlfrescoTransactionException.java create mode 100644 source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java create mode 100644 source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java create mode 100644 source/java/org/alfresco/repo/transaction/DummyTransactionService.java create mode 100644 source/java/org/alfresco/repo/transaction/NodeDaoServiceTransactionInterceptor.java create mode 100644 source/java/org/alfresco/repo/transaction/TransactionComponent.java create mode 100644 source/java/org/alfresco/repo/transaction/TransactionComponentTest.java create mode 100644 source/java/org/alfresco/repo/transaction/TransactionListener.java create mode 100644 source/java/org/alfresco/repo/transaction/TransactionUtil.java create mode 100644 source/java/org/alfresco/repo/version/BaseVersionStoreTest.java create mode 100644 source/java/org/alfresco/repo/version/ContentServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/version/NodeServiceImpl.java create mode 100644 source/java/org/alfresco/repo/version/NodeServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/version/VersionBootstrap.java create mode 100644 source/java/org/alfresco/repo/version/VersionModel.java create mode 100644 source/java/org/alfresco/repo/version/VersionServiceImpl.java create mode 100644 source/java/org/alfresco/repo/version/VersionServiceImplTest.java create mode 100644 source/java/org/alfresco/repo/version/VersionServicePolicies.java create mode 100644 source/java/org/alfresco/repo/version/VersionStoreBaseTest_model.xml create mode 100644 source/java/org/alfresco/repo/version/VersionTestSuite.java create mode 100644 source/java/org/alfresco/repo/version/VersionableAspect.java create mode 100644 source/java/org/alfresco/repo/version/common/AbstractVersionServiceImpl.java create mode 100644 source/java/org/alfresco/repo/version/common/VersionHistoryImpl.java create mode 100644 source/java/org/alfresco/repo/version/common/VersionHistoryImplTest.java create mode 100644 source/java/org/alfresco/repo/version/common/VersionImpl.java create mode 100644 source/java/org/alfresco/repo/version/common/VersionImplTest.java create mode 100644 source/java/org/alfresco/repo/version/common/VersionUtil.java create mode 100644 source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoService.java create mode 100644 source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoServiceTest.java create mode 100644 source/java/org/alfresco/repo/version/common/counter/hibernate/HibernateVersionCounterDaoServiceImpl.java create mode 100644 source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicy.java create mode 100644 source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicyTest.java create mode 100644 source/java/org/alfresco/repo/version/version_model.xml create mode 100644 source/java/org/alfresco/service/ServiceDescriptor.java create mode 100644 source/java/org/alfresco/service/ServiceException.java create mode 100644 source/java/org/alfresco/service/ServiceRegistry.java create mode 100644 source/java/org/alfresco/service/cmr/action/Action.java create mode 100644 source/java/org/alfresco/service/cmr/action/ActionCondition.java create mode 100644 source/java/org/alfresco/service/cmr/action/ActionConditionDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/action/ActionDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/action/ActionExecutionStatus.java create mode 100644 source/java/org/alfresco/service/cmr/action/ActionService.java create mode 100644 source/java/org/alfresco/service/cmr/action/ActionServiceException.java create mode 100644 source/java/org/alfresco/service/cmr/action/CompositeAction.java create mode 100644 source/java/org/alfresco/service/cmr/action/ParameterDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/action/ParameterizedItem.java create mode 100644 source/java/org/alfresco/service/cmr/action/ParameterizedItemDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java create mode 100644 source/java/org/alfresco/service/cmr/coci/CheckOutCheckInServiceException.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/AspectDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/ChildAssociationDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/ClassDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/DataTypeDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/DictionaryException.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/DictionaryService.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/InvalidAspectException.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/InvalidClassException.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/InvalidTypeException.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/ModelDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/PropertyDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/dictionary/TypeDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/lock/LockService.java create mode 100644 source/java/org/alfresco/service/cmr/lock/LockStatus.java create mode 100644 source/java/org/alfresco/service/cmr/lock/LockType.java create mode 100644 source/java/org/alfresco/service/cmr/lock/NodeLockedException.java create mode 100644 source/java/org/alfresco/service/cmr/lock/UnableToAquireLockException.java create mode 100644 source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java create mode 100644 source/java/org/alfresco/service/cmr/model/FileExistsException.java create mode 100644 source/java/org/alfresco/service/cmr/model/FileFolderService.java create mode 100644 source/java/org/alfresco/service/cmr/model/FileInfo.java create mode 100644 source/java/org/alfresco/service/cmr/model/FileNotFoundException.java create mode 100644 source/java/org/alfresco/service/cmr/model/package.html create mode 100644 source/java/org/alfresco/service/cmr/repository/AbstractStoreException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/AspectMissingException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/AssociationRef.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ChildAssociationRef.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentAccessor.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentData.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentDataTest.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentIOException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentReader.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentService.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentStreamListener.java create mode 100644 source/java/org/alfresco/service/cmr/repository/ContentWriter.java create mode 100644 source/java/org/alfresco/service/cmr/repository/CopyService.java create mode 100644 source/java/org/alfresco/service/cmr/repository/CopyServiceException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/CyclicChildRelationshipException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/EntityRef.java create mode 100644 source/java/org/alfresco/service/cmr/repository/InvalidChildAssociationRefException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/InvalidNodeRefException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/InvalidStoreRefException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/MimetypeService.java create mode 100644 source/java/org/alfresco/service/cmr/repository/NoTransformerException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/NodeRef.java create mode 100644 source/java/org/alfresco/service/cmr/repository/NodeRefTest.java create mode 100644 source/java/org/alfresco/service/cmr/repository/NodeService.java create mode 100644 source/java/org/alfresco/service/cmr/repository/Path.java create mode 100644 source/java/org/alfresco/service/cmr/repository/PathTest.java create mode 100644 source/java/org/alfresco/service/cmr/repository/StoreExistsException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/StoreRef.java create mode 100644 source/java/org/alfresco/service/cmr/repository/TemplateException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/TemplateImageResolver.java create mode 100644 source/java/org/alfresco/service/cmr/repository/TemplateNode.java create mode 100644 source/java/org/alfresco/service/cmr/repository/TemplateProcessor.java create mode 100644 source/java/org/alfresco/service/cmr/repository/TemplateService.java create mode 100644 source/java/org/alfresco/service/cmr/repository/XPathException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverter.java create mode 100644 source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverterTest.java create mode 100644 source/java/org/alfresco/service/cmr/repository/datatype/Duration.java create mode 100644 source/java/org/alfresco/service/cmr/repository/datatype/TypeConversionException.java create mode 100644 source/java/org/alfresco/service/cmr/repository/datatype/TypeConverter.java create mode 100644 source/java/org/alfresco/service/cmr/rule/Rule.java create mode 100644 source/java/org/alfresco/service/cmr/rule/RuleService.java create mode 100644 source/java/org/alfresco/service/cmr/rule/RuleServiceException.java create mode 100644 source/java/org/alfresco/service/cmr/rule/RuleType.java create mode 100644 source/java/org/alfresco/service/cmr/search/CategoryService.java create mode 100644 source/java/org/alfresco/service/cmr/search/NamedQueryParameterDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/search/QueryParameter.java create mode 100644 source/java/org/alfresco/service/cmr/search/QueryParameterDefinition.java create mode 100644 source/java/org/alfresco/service/cmr/search/ResultSet.java create mode 100644 source/java/org/alfresco/service/cmr/search/ResultSetRow.java create mode 100644 source/java/org/alfresco/service/cmr/search/SearchParameters.java create mode 100644 source/java/org/alfresco/service/cmr/search/SearchService.java create mode 100644 source/java/org/alfresco/service/cmr/search/SearchStatement.java create mode 100644 source/java/org/alfresco/service/cmr/security/AccessPermission.java create mode 100644 source/java/org/alfresco/service/cmr/security/AccessStatus.java create mode 100644 source/java/org/alfresco/service/cmr/security/AuthenticationService.java create mode 100644 source/java/org/alfresco/service/cmr/security/AuthorityService.java create mode 100644 source/java/org/alfresco/service/cmr/security/AuthorityType.java create mode 100644 source/java/org/alfresco/service/cmr/security/OwnableService.java create mode 100644 source/java/org/alfresco/service/cmr/security/PermissionService.java create mode 100644 source/java/org/alfresco/service/cmr/security/PersonService.java create mode 100644 source/java/org/alfresco/service/cmr/version/ReservedVersionNameException.java create mode 100644 source/java/org/alfresco/service/cmr/version/Version.java create mode 100644 source/java/org/alfresco/service/cmr/version/VersionDoesNotExistException.java create mode 100644 source/java/org/alfresco/service/cmr/version/VersionHistory.java create mode 100644 source/java/org/alfresco/service/cmr/version/VersionService.java create mode 100644 source/java/org/alfresco/service/cmr/version/VersionServiceException.java create mode 100644 source/java/org/alfresco/service/cmr/version/VersionType.java create mode 100644 source/java/org/alfresco/service/cmr/view/ExportPackageHandler.java create mode 100644 source/java/org/alfresco/service/cmr/view/Exporter.java create mode 100644 source/java/org/alfresco/service/cmr/view/ExporterContext.java create mode 100644 source/java/org/alfresco/service/cmr/view/ExporterCrawlerParameters.java create mode 100644 source/java/org/alfresco/service/cmr/view/ExporterException.java create mode 100644 source/java/org/alfresco/service/cmr/view/ExporterService.java create mode 100644 source/java/org/alfresco/service/cmr/view/ImportPackageHandler.java create mode 100644 source/java/org/alfresco/service/cmr/view/ImporterBinding.java create mode 100644 source/java/org/alfresco/service/cmr/view/ImporterException.java create mode 100644 source/java/org/alfresco/service/cmr/view/ImporterProgress.java create mode 100644 source/java/org/alfresco/service/cmr/view/ImporterService.java create mode 100644 source/java/org/alfresco/service/cmr/view/Location.java create mode 100644 source/java/org/alfresco/service/descriptor/Descriptor.java create mode 100644 source/java/org/alfresco/service/descriptor/DescriptorService.java create mode 100644 source/java/org/alfresco/service/namespace/DynamicNameSpaceResolverTest.java create mode 100644 source/java/org/alfresco/service/namespace/DynamicNamespacePrefixResolver.java create mode 100644 source/java/org/alfresco/service/namespace/InvalidQNameException.java create mode 100644 source/java/org/alfresco/service/namespace/NamespaceException.java create mode 100644 source/java/org/alfresco/service/namespace/NamespacePrefixResolver.java create mode 100644 source/java/org/alfresco/service/namespace/NamespaceService.java create mode 100644 source/java/org/alfresco/service/namespace/QName.java create mode 100644 source/java/org/alfresco/service/namespace/QNameMap.java create mode 100644 source/java/org/alfresco/service/namespace/QNamePattern.java create mode 100644 source/java/org/alfresco/service/namespace/QNamePatternTest.java create mode 100644 source/java/org/alfresco/service/namespace/QNameTest.java create mode 100644 source/java/org/alfresco/service/namespace/RegexQNamePattern.java create mode 100644 source/java/org/alfresco/service/transaction/TransactionService.java create mode 100644 source/java/org/alfresco/tools/Export.java create mode 100644 source/java/org/alfresco/tools/Import.java create mode 100644 source/java/org/alfresco/tools/Tool.java create mode 100644 source/java/org/alfresco/tools/ToolContext.java create mode 100644 source/java/org/alfresco/tools/ToolException.java create mode 100644 source/java/org/alfresco/util/ApplicationContextHelper.java create mode 100644 source/java/org/alfresco/util/BaseAlfrescoSpringTest.java create mode 100644 source/java/org/alfresco/util/BaseAlfrescoTestCase.java create mode 100644 source/java/org/alfresco/util/BaseSpringTest.java create mode 100644 source/java/org/alfresco/util/PropertyMap.java create mode 100644 source/java/org/alfresco/util/SearchLanguageConversion.java create mode 100644 source/java/org/alfresco/util/SearchLanguageConversionTest.java create mode 100644 source/java/org/alfresco/util/TestWithUserUtils.java create mode 100644 source/java/org/alfresco/util/ThreadPoolExecutorFactoryBean.java create mode 100644 source/java/org/alfresco/util/debug/MethodCallLogAdvice.java create mode 100644 source/java/org/alfresco/util/debug/NodeStoreInspector.java create mode 100644 source/java/org/alfresco/util/debug/OutputSpacesStoreSystemTest.java create mode 100644 source/java/org/alfresco/util/perf/AbstractPerformanceMonitor.java create mode 100644 source/java/org/alfresco/util/perf/PerformanceMonitor.java create mode 100644 source/java/org/alfresco/util/perf/PerformanceMonitorAdvice.java create mode 100644 source/java/org/alfresco/util/perf/PerformanceMonitorTest.java create mode 100644 source/java/queryRegister.dtd create mode 100644 source/test-resources/Plan270904b.xls create mode 100644 source/test-resources/cache-test-context.xml create mode 100644 source/test-resources/farmers_markets_list_2003.doc create mode 100644 source/test-resources/filefolder/filefolder-test-import.xml create mode 100644 source/test-resources/quick/quick.bmp create mode 100644 source/test-resources/quick/quick.doc create mode 100644 source/test-resources/quick/quick.gif create mode 100644 source/test-resources/quick/quick.html create mode 100644 source/test-resources/quick/quick.jpg create mode 100644 source/test-resources/quick/quick.odt create mode 100644 source/test-resources/quick/quick.pdf create mode 100644 source/test-resources/quick/quick.png create mode 100644 source/test-resources/quick/quick.ppt create mode 100644 source/test-resources/quick/quick.sxw create mode 100644 source/test-resources/quick/quick.txt create mode 100644 source/test-resources/quick/quick.xls create mode 100644 source/test-resources/quick/quick.xml create mode 100644 source/test-resources/quick/readme.txt create mode 100644 source/test-resources/testQueryRegister.xml create mode 100644 source/web/WEB-INF/web.xml diff --git a/.classpath b/.classpath new file mode 100644 index 0000000000..718fc4c88a --- /dev/null +++ b/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.externalToolBuilders/JibX.launch b/.externalToolBuilders/JibX.launch new file mode 100644 index 0000000000..4a04fcc9eb --- /dev/null +++ b/.externalToolBuilders/JibX.launch @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000000..7e072e9002 --- /dev/null +++ b/.project @@ -0,0 +1,28 @@ + + + Repository + JavaCC Nature + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.ui.externaltools.ExternalToolBuilder + auto,incremental, + + + LaunchConfigHandle + <project>/.externalToolBuilders/JibX.launch + + + + + + org.eclipse.jdt.core.javanature + rk.eclipse.javacc.javaccnature + + diff --git a/config/alfresco/action-services-context.xml b/config/alfresco/action-services-context.xml new file mode 100644 index 0000000000..7550045a0d --- /dev/null +++ b/config/alfresco/action-services-context.xml @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + alfresco.messages.action-service + alfresco.messages.action-config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + diff --git a/config/alfresco/application-context.xml b/config/alfresco/application-context.xml new file mode 100644 index 0000000000..ac0217ca27 --- /dev/null +++ b/config/alfresco/application-context.xml @@ -0,0 +1,863 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + classpath:alfresco/version.properties + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + classpath:alfresco/repository.properties + classpath:alfresco/version.properties + classpath:alfresco/domain/transaction.properties + + + + + + + + ${db.driver} + + + ${db.url} + + + ${db.username} + + + ${db.password} + + + 20 + + + 20 + + + + + + + + + + ${server.transaction.allow-writes} + + + + + + + + + + + + + + alfresco.messages.version-service + alfresco.messages.permissions-service + alfresco.messages.content-service + alfresco.messages.coci-service + alfresco.messages.template-service + + + + + + + + + + + + ${mail.host} + + + ${mail.port} + + + ${mail.username} + + + ${mail.password} + + + + + + + + + + + + + alfresco/mimetype-map.xml + + + + + + + + + + + + + org.alfresco.repo.search.Indexer + + + + + + + + + + + + + + indexerComponent + + + + + + + + + + + + + + + + org.alfresco.repo.search.IndexerAndSearcher + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.search.CategoryService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${dir.indexes} + + + + + + + + + ${lucene.query.maxClauses} + + + ${lucene.indexer.batchSize} + + + ${lucene.indexer.minMergeDocs} + + + ${lucene.indexer.mergeFactor} + + + ${lucene.indexer.maxMergeDocs} + + + ${dir.indexes.lock} + + + ${lucene.indexer.maxFieldLength} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + alfresco/model/dictionaryModel.xml + alfresco/model/systemModel.xml + alfresco/model/contentModel.xml + alfresco/model/applicationModel.xml + alfresco/model/forumModel.xml + + + org/alfresco/repo/security/authentication/userModel.xml + org/alfresco/repo/action/actionModel.xml + org/alfresco/repo/rule/ruleModel.xml + org/alfresco/repo/version/version_model.xml + + + + + alfresco/model/dataTypeAnalyzers + alfresco/messages/system-model + alfresco/messages/dictionary-model + alfresco/messages/content-model + alfresco/messages/application-model + alfresco/messages/forum-model + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + ${dir.root}/backup-lucene-indexes + + + + + + + + + + 5 + + + 20 + + + 60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${system.store} + + + ${server.transaction.allow-writes} + + + + ${spaces.store} + + + + + ${version.major} + ${version.minor} + ${version.revision} + ${version.label} + ${system.descriptor.childname} + + + + + + / + alfresco/bootstrap/descriptor.xml + + + + + + + + + + + + + + + + + + + + + + + ${spaces.store} + + + ${server.transaction.allow-writes} + + + + ${spaces.company_home.childname} + ${spaces.dictionary.childname} + ${spaces.templates.childname} + ${spaces.templates.content.childname} + + + + + + + / + alfresco/bootstrap/categories.xml + + + / + alfresco/bootstrap/spaces.xml + alfresco/messages/bootstrap-spaces + + + /${spaces.company_home.childname} + alfresco/bootstrap/tutorial.xml + alfresco/messages/bootstrap-tutorial + + + /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.childname} + alfresco/templates/software_engineering_project.xml + alfresco/messages/bootstrap-templates + + + /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.content.childname} + alfresco/templates/content_template_examples.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.configuration.ConfigurableService + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + classpath:ehcache.xml + + + + diff --git a/config/alfresco/authentication-services-context.xml b/config/alfresco/authentication-services-context.xml new file mode 100644 index 0000000000..2f8bce9be4 --- /dev/null +++ b/config/alfresco/authentication-services-context.xml @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.security.authentication.MutableAuthenticationDao + + + + + + + + + + + + + + + + + + + + + + + ${user.name.caseSensitive} + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.security.AuthenticationService + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.security.authentication.AuthenticationComponent + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.security.PersonService + + + + + + + + + + + + + + + + + + + + + + + + + + + ${spaces.store} + + + + + + /${spaces.company_home.childname} + + + + + + + + + + + + ${server.transaction.allow-writes} + + + + + + + ${user.name.caseSensitive} + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + P1H + + + + false + + + + false + + + + \ No newline at end of file diff --git a/config/alfresco/authority-services-context.xml b/config/alfresco/authority-services-context.xml new file mode 100644 index 0000000000..0cfee86d48 --- /dev/null +++ b/config/alfresco/authority-services-context.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + admin + administrator + + + + \ No newline at end of file diff --git a/config/alfresco/bootstrap/Alfresco-Tutorial.pdf b/config/alfresco/bootstrap/Alfresco-Tutorial.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0326c06e2413610e8ba8924dfd28900e76eedaec GIT binary patch literal 3285790 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

Zycsg8C$tUkj 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

oEe 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

scQwF=UW=MFB9}#*nBF5R{8|UL@{7vix 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

g}iEqj!a#VNFYZQI#;> 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�*GJ#yFPbGvCaNcZpMh4FBIpDbNo5f$?vAYG~-&G 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 #IK>=%)~@;KqVMzxhiF?FLYl$x) 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%0Dkm`dWp;v8kifLLSHn! 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

` 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


f?4OS015|e1l^~ia=_mEi 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 z

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

B(N@6aBVkLsGH=-rO#$%#~WzB6qvbCXT9R# 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

  • =a!b2kffk9KDi)KcPQdy#`E@-RT^bxc|<-Q1GqOezK;il`pD18rhm4rV;HB!H-&G!IORM%?~9TKH-R@N>Aeyo&1w@Ch#T+ksL!OP zr{s~_?TIoq$RQ8~0?2J#m!db9Lfl0;auQ9lP=mN5|DMw~B-wQK=v?}gI%O>AZkNZq zk&wRqIA<(XQ`FYAcY68_;2Y%c_?AcM)I|^_$?~DsooRmEPj+2}7#_ny_q^UDB!e!9 z`{v*GrcDgjQ!oyfqpuxAnHRk|mM0Ch0{tEPR4Lr>n;5~hVXdiDyUDxfS>)3_-Tn8W z^!HnoNe(`CU#15?@lrS1pOq@5 zx;gOl;@K-W-$ObC&?Q!e7?coz%tg^=GgQ~5<8C7UR>|pLMmks~w|#cTJx&MsIIq{J zm8uIx7pBm+qm2a6(`51KzPS6BRxtgP`n}O1<|Zjx!QN|0Nu{kJQOqW^ckjtQ;wLcQ z#E10d)gev^e{w&29v1AahC23lfHd36NDn_?8(IQmi2eH*O5FtN1Ec`}h_tDPjbXm* z&(rZv_Wu~2Kwy|SP|ERWMN{8f^@rGiT>pdL$B_6CFfjCf3=GFj8L>>DwQ zboPK|Z}o?b__w$Je{izo5Rlss+xJ6m|1oGqKjhZ;LvG)V&=0x&$HZ#=M@zupwd9AT z{h=k_)r?5*hnBD*04@2`hJV+RAAUg7=7*O2+uQdM@ITJUPXY7q%G=ECv(SFF+64kp88kx!4Rc!jt3BV?xNh(w$qQ6evJ2H1yo+eRH!*?nydF?sau} zlo~=3mouVD=A#NaUojz3H3HIg3@GA&=6Kk#yo*mf%MJ`D$l-`LVtM{?Nh9oI35Yj1 z_yXiUlO>7s*3;DQEt@2_2JL|~g_S<xnA{ z6sfN2EOC?l+2aP z^g&{Zn?=69zK=00%dHOxvY!VgL#s$W+hn9?8&KzR=-jG6ym=~r;Hv9Y1A3seW#K)% z^t(4t5+U_gdNH9Z)016QPtN!h42)mLw*&80p-13O z-5mSC&++$=MEWWY5Ztq;XLap)b|&v(y4RvmREeHg$Pl{1y7eX#T{3c=VvQLiNRGAm zoJeiuB!zb$QE^h1M>w8Yj}w$;)Z zRod$#Pk~Rep{Up;dndiO;* z`&B+|Kd?}9v7V>ri`W$X-4~&+l7^LNXS0<^!{o)9ttmWhW3H)BemME^dHC#ipti*u z`u*H0Vvb4kj+DEXbYn0>9NP?<&=cMEGYp>EP_uD+-U+hMWK8vwZ&CHe%Q1}PFG+Vtgt>dNaiwS&JA$ZXC6ol)m}434(V@N8u$?%%{r24V zciO3MatfNrkEbsdT!`E_T)Gd!$C(h3@!lOv0h?B9Wk0`Ph>By*IvL19RI<`{&qF*VE`Hgxuza~7;iIMi7xmx@{ znLDC#jW_aT+oQ<8r3?xw+|nxNex9r>PZ)VUFNe7o8D6_|7F*nU25C5NE~h^Rr-&_3rRd^ZU>)gqs-2_ZY-aP z(P9u(IqCUIEN3og{8swjrz_~ZShlu#59A4L(OV@y!*pwn1aCbI^;olH-{B}Nuasg4 z%N-Pg7vh(2a1Wye2b=6?9sV37mkff1KEgN0p3g1E{@S}~+xJUJnlIoByRR{Pj8w!E zT{DP6^;#=g>H7E-6nGNvfi6H=q@YSnF`=d0pEku}68xv>8nm)~)KuBW3WKu3lK)mR z>Kz=7WQ*Rai{9#&5wiKqLRl=tKKb(n6$Frt=DVq(U_>tbus@;lZYW9GWz^bbV3P~b z)E@@37_ue%!@T|5_`eUs=;4S=I3nwe16=hVXPIi+c0~Ulhv%eNi1Y8y{~NL;%F|6- z3E2o_(6m;WaQt)lb94j>i*nW22^5#E`hn4uWVFYx=v`+~G_lUfx}gr-(s_KBL#<8kQEwWwfF+?p+TR7pE2gYw}Wf~d45WA93xjyr|-t5HO_pT zy#Ps*hb}ZYXtoS1o|U3=J@X?(8+e{QoW(E@w^;t-n_#2o8tVLuU@lQy7Meoo3y{Pa z*jCTK~v4L0vS1;id{0*`XGMC3OYaTh8D$ z4ParOF)c9`P`ovGwH5xgtT7Ayph1g>gU4!AAeCSFVv*7*?M{L`tFUdG~YL5?)uBiwpu!NEr2k-FL9WRQ{D*3})lBk3y zOoJD7rtKOI8k|`4@4)I7dRqe02shqO*ul7N8Md7as6E2;K*8=W6nx@|9>{v%GmVN9 zDH(!6+)LHMw75f{vbeU=HS4g5fy!N09&pxdBgy=7u>ItBWo3 z$t89>%Wo)X*y=xRq@FLz4>SagwzMBu+q1~q-42BDI|L4Peh|>Ot3O|7lTDvC&$dA3WN&|%)0&VAy-W_;QZ)YmvVIJoable6-C0}mH; z87$%Y{hkmJ=wlvXu+FmgU@|!jZlm;Bb8M!XlP&aRFtu{S(i=*K|R&)!yfOc5<56 z>Av20@#T5_!GdFtrBTr)UqZE9-eDZIT)H-G5eUk3YnjS9atNMRqizztP$`!omxynm z`_gTmjOH;!N^?xJ^h_|xQw_!=amAC!lNMii$H9x*iUvQslz?;legd3a#PfDzc-dYd zB~`9t5|9Qy<4!F5RGm$^n(F(*W_XX)q{4@V7nM%$u^Q0Hr3B88-7)sm9c@3@kuj3B zjZq?)S31|oNO=m|X%`4|8D*lCZO<$H?)x*$=-t4<)d>!NjV7k7WG#8E0o{>vI^-U- zUL92=YrgD7dQ@1@3Xi-f2s2Am<7y^L3qwWNmq6LzvMyT6Gk^Mn46D`84ZX`+GTLh4 zd~Y0{$aa2+;2opNAs;q)JYBkelWm~AIQJ$ZwNnr$19vwhW0?}<UyRwk4WBPm4?Jtqh@2) zii#+nS1Oi6UcknRDV&?0Lk<1SanS;l^*@c_qW-Yq{Oe{8j@<_D9Jn(8V`QkM}8_n}J<&giy-op?SVe2g&HRZ0Wq%KU|NMBn=IVt{T! zC8dXhFsj?XcaU$lx3`iaYeoV%*;C)! z!aQC#aV);A#AzDF{Ssm_*~ZroOAvh7Wo`Gw^>d((#ui#MTOwj6ZX5QVI>7lF5Z}sq z<@l3j5CJVyHg_<-bE$KDs-Ldz%7# zl~EdCmdyq-eRJ^|M+0hR-)_}FAEp)%FQHL#Ah!fg3u(r;;D>13GR()RgrI+a{LTao zk`Sg~_9|6YY7}|~_g1S>me7yk_tjQY%`MtNXfHP7A7GrZB|%`KeKR(#vva-RXz`vj z?&wMuQ}uRq6Woaw@TRJ{oyxlsNGvoV-SOZslW!qV?MRV=UQUiS5&G=mmDbn`5VbMl z2uSVTLui@2y8>SSlzd9|v2t;j`yK4w){qY7#2%x_T?nLHS{DvI2LhfkFF+e>*9py) zA=!|P5#DzUae9*&JmFREYB#LctkYlVe~h@*A#D(}NON*RbBuQZni%-`aa3us%JW*n zyE|>_iJV_qNGfWIr6ZW%-*tN1cI%Dv%gLE)Waj3smD6)a!@2&cpB=9J{J54LoyC4y zEPUclfY?W-!U{I?<)vFDwp?2k6jq>};5|5ahX$C9r2hPvQBHV68MLli?-H9yHM;3{80XlT?{OhBuT)mU0gKSj~i++b){R73e^(n3VAcfU;*&Wq9 z%TbW!1;|%0;In0aepCZtp*vltu)b*o2683#0gHut2(lM?>2v&&?j7RMY;vQ!H)dT) zfK=wMWjO+gO>AY(35}1))n3>7YN=(Jo8+6a#JDLe%%}o;^WhgD-wP1pJ@w@;(wWCT zdbfEjNs7q`%{Sn+o3oOd`yooMgeI==E&8)nglri>mV4*Ev8LFTR&H1A<+i^7k}nTQ z#nJWq7+gX*nEY)0ZcN%LIXHXlb{FMJCRahqgIVz>L>9tPZ(`Ttb>N;R018l>d35#C-5@{A-YzW>hJ{Hrtnj@AE8b_NGd+g)BkP;m545W&3e|FF{u zPf7ykZ+*|2QW<*wvA7lplL9Iv?hAR8+0rpVAk=ydPV=|sedG#>ZXb*iQm=i^taYmA zhPr`9Ah@StY(ETn1k>?Md)|F3D1*^9Xy&&DTs}~yG0RL4Eh`r{<@aX(8eFhjtKtUd zQM4e!67rYRkwgDmDpr5KA^X!mp#0(-fB9ng(=!cRc7Y(3OOu_;6Q9_h&I$6DfAhWh zb8>cRo|bmmG12@~ybu3RBOkQkc!G3KtTp4V-uOP{lK&~f{ysXKE^9}iY@}x-bTFyX z6eojrpAUs2j@#mj^I3;bEAL*?u-ar% zr*S10lV+0oXBlv#AR0Ww0!;w#Km`$B^deGaIQ_m`KvP7TGEYYML4gVywMA*g1<23w z+am?`4L5;D{9iuIGH&ZT$57%mX5C4?lUd-^OOyttD_nW`;bYU|XBeLi=FirI&Sj6) zR3Ntlqf0&FF>j|jU2V%;hvkn%9ZV$*IBJbAZ||Rdp?_2wC!;UE*~pvtp;cv#)z(gZ z^A1efA_MIf4dreUcZ=D?TI-%#jL5|OpejlFYZ}JT`Wse;M50gTC_5Z-D}8wS#&Nu~ z%APFW?)wsup(-h^l>fL2Bc7?g!+mu<@LjH1bWr;m-V+wq1JjV(%H#sQ&zbyAzeyGn znc1PV(N%p*>+*Pib8+b?D7-cHVUFO*=?&o#)*hilfqqKk-5k1A+7KgO&(|{8)X501 zl;eY|j)=AQng7614F4^vG31SMh zfu&Bhrql9M&`3fBUcJ~Oq2Ul)@6mQHEk`J0l`lob1YUqVy3VjRw(j3&p1LZ#-B$4B zsR3HXx|eHb;`0xiki!>7b#o`$Qbc7f^N8%lLz!$JHwsOXs^u@vW6m5U-@N7WUYva{!VE3D|<@zax=k7SLLGFp3Sng^yP3`>K% z58=<&#}>ufP7ZR&uY})5w8!C<%D-=!qG86%>SKc_l;j(>9tBYbqO0=CGCJ{K3hpoO z=h@>uX0P=h6vEiljGSccN754wQJPc26@@}|yWBZEpb;k<3dimpv0vlW7rwSjlQ_GF+RqJk-I1W5jeH5K465Ykvknn=tNe^QBj7Lcj_h?)(Y=$7hI-r& zj=`v0ZHyLX1dTGCckOc~hwO(Ja|<}V?blDoH~4}&l9s+J`6B57~MW`t2si%7d;NI&OnzSF~Ux|NA+1R4QiEL_I~E2#1flQiYFk1<@!^ z7k0IVID1x$JqIzZ<|f-37<^Mc8?VF+tvk^diLZ`uDhOT z$Y`+h;24vs_suM46z%8quf;!aR$?O=9c&GyHC*~weeb8q`u9X?jLhBDR z_iNOX4Ph{@!LQc!QI?-PdVZSd?8!H|Y6j#(@hao`R7h+OA;2@4_V58G=$nbSH6WotjCD+|brpPs9#NHp&Yee%#} z7az_eIGgk(n`G#~VlzK?)2Eltll*?7ezC$?vZMMd5vQvL^$~)!UTPIPE#pz5tyKO` zZ-wMDB)<4e+X6?E~@G zo9*p=X%e4LiA=q*ORJ_**b};D?|hYGk=k6z^?Z7yQf^c}UI;%q`|CZ#}DUJ?j*j z4)fEOh>Y`_~yvD(76|*DPv}$<4c5)E!$!| zoF}-4FTVk4@WMgJZQ*7jE;H_QP)FXx)d%h*0;3y4d=GOYGc5z(X8?!dW#OS-n%K!i zlMP~J)%=OP58zuQca3N>p(V6m7XwzfcwhDrX`MBlhV(>f4YcQxm0y!G=)jU!R^-+o zjbsQV{c1cll2uCaj@|Ta4MPXU@UzI^E?U#gAu327H+e|TYRa3tjj6St}+w zr)ZeR{1=fkwWi)AnhHJ7^0mh%d$BH7!ZBoDpIg)G_TR3q+xr&4=r2cnmG17)BVWRz z3sB6HH<*rJUj%y(%SzVq>U%=W0%yGyDesttn?Sh4l^437Y&Kl$u308q^PMoTR&;$4 zt5)*Vq2(5xtGKl}>0?hJvF!BfZ=^0pjfSkZNUrFMh~x_3lQ^5x~^F_c%ZoF z&~rW5>~Km?DPSU)g)DSI(P{&KPBqCGW zn(h1OPId0+&SQa~DP(^!Ns#$rHKa^Fo;Y|l(#wdI(erjDYk(alqLbs*jC9K8MtUl5)21H2DP%2bnmloQo z&|mW;JAaXV0on<(n2}9JHI+*s}G?Yn;vsfDaAwzHv z8Ix3aWpK2h=5**xD&aVJP@bh-ANI2afwIuUi7i56 zZnkWFkb^RBQdtNjZ7p_bE}nw4XS1?A;bsY`1}{~++x52ViTIU3RlqTEs)*et0&pZ%ygEGu><7T;v*c-kx75vp$ zHreb}VpdJX{WT(X!D7(8s(W#yEWt4 zXXBb>w|%P={%Vk}bP#J^%T=x08z($jAJ-$70)neT6_Y|ydd0fSn6!a`o+P|;nGA;Q zEB+osZ=`nkj6I2ytHb@Kg@ zsCRg#n5RR1eH{&mLre~d#Wm7I+alx>8KiXe{>pfA^a&Z}(C++j@MyHwFuN9f@ zw(Rd14Y`+N@f-6$p@b^S$O!gcaXcPAdWgkoK=_o>9JyizsDsh6#v1(b`hr)|$osh!D!w^}K&jqMX3TOGnsipH z=2w*+^A0Uaiq0a`(<1Pkkg*5Mhs2fA9%^o8TA*Mo6EVEOOdU&EwUR)%mZGZQ9(^FT z$6F2hMI2puMk+7Y!yd!22r}Xx4pIg@Ue+Q(5UB|jguH#IjBFnPJ`?zwi@fLga2lpx zk4;OeraAu9w=FaKh*E28uflyQz-dq1SfO5Q=yNem4hwcshuvYuwo z%1ssagF4ujmS?jz;~N=MDF!_>n|VR1>ZQ*LjU9K5Q9Xywc9Yq&oq3fwXI1AKBt@rnM@_+ffLsr;!BcYX6ANm zn7)bxagre^;bLFb6+5edjpewy9ZH~7kc+1__)ip~%;pOhiP*dCd7i?9Je^ldhr?F) z^eep!BDj0I;nv!}YnL?=d0S2H;Rhg%Jz=$Ft!Cf(IZ&9*cNw^dlzO?1Yj{d>uI1It zRx*mNN=dl5AbtOtCaLQeXL%L6wT7*54{iOTwqb~>-pubo;9jlHBTGn6SbD@8S{185 zI`y%^p(rwZ`F(bs{}l+!On<4|eq3CMX9bTl!<)Lo!#wbB*+h>);*&yd#c@_NX+;s_ z$aC1s-FHvyy8LBUismGroCE=9Q^0Lf5-kg(uBUAGPfYv0h^D~Y50u1p<2+w4P$#%S z+xV-0+e|T<{7kpZI{XprEeRvW+cqlDK9M-gJ{pi4m>cp{2q^Ef0?&smFL9n@0vD}# z>7z$nyidxmej;vkF|N`Pr1Wy0d1p$6sJZc|>8WG6RPPRm>hsd)+20b@q8jM_0QeUf z;hk6$?^8&*;m@Ou&Ql9UNtz$cU5O`ZCTWE4b!lm^glA0$AKj@qzf7g^V> zofw|ueF17&{N+J^v4|tBPA_ZZ6Y2Ra>66X`vdCa;Bhbt!<8vzyKE0qUVkPDK+$o(6 zG}DYzM|lOmMQczephGvV>AEt4kX3kn!^ z8+nrY-{(DEC3+lUero35PflD=h@hwb7ohxN$cn8rG3YHRH+q0c2O8PQWEoKhaC9_7 zN>&ny1TLwRXm~BcTHF_+aqf%wwmOl9LH5aG8*5 zQTq`}$82NROVL}aQv?U4uHM{y;a8&HqdCz!aSiQ-D@e4np)%Il| zOU%wjo?ZFw^o2C$vRp&go!ogbiw^eF_WA_5iE)brx`4cS2&M6jGd}K zLfGF)-j@2xOpi%Rx%2$`qXdOL+cKtS8_z2v-`6=ak&Gq1+tdx26Fgr}S1H7(+{22_ zm8?`d>Dxxb367l1AlsJ5>3^wPRQXxYViCgqE?oZg2wAL16gP!DsVG{~b;A~8%g@qZ zWA1#3zlHIoyF}`mw0{?`ukI1P3~3@r7^*Pcf=b^)n#C={genS5*a(e$|NZ*^2{(9w z5VjF8!VS21DP_fM3m)kJmWjZ@H?T}xh}keT#FmI9j$nITkAl~}@#eVNkJb82VD@Z` z=J+NfP;6OO9Jo*kwS-I?BH}-j!;%|cm9s$1fcS8)`3n##5V{2vTJEVp9<(9R0k|!i zpZ%A%u%)j%vb5qoNGi)#s;0HQ?Iw*#ut%&z&#y)TrI=GMgIAk0XmER-x2OwRe>s?* zlKwvKPW>$82l(akIbL-0-?s}RKvkOq8{3l?62vm!%r8sBCDf?|#nvgk$LCEq3 zWcE6e5b{f_iNM8zjY-J1KIH3zs~h63!pHIUZdW zlImq1W!LRQSc3Ns!P|g~xCXw&Bi+|shQDwEd>Ls??Ms}Hy@YWPSJf{XBzp;EV8{A0 z`s31?piA!t|kA23$Y6XW&Lm1_@=d&SCOjF zGxtyd*YkSXL5Q6Wq)#5a-JE=`1XB7fK@TrLmtLfqjG@^ZR5{xPW?45SZvKPc(*R$7 zRrYZ4&G~*L@*ME?&Hd=J|G-|n%dZX~s-9ke#DMdGx*L#PK9swc-16m_gw(I^{Yx9s z{~mIaxAw&N7UL&zye7&`R(r)fU7sH~?eUhMUJb%!o-$73}83|M}$x}xIXRzz6NlGDSa zS2THGEq^7MHA@bfE~W)L=f=GuV5yl&ZG1j5*R+75$f^mt^#3vDxKRK|$LSKK2XJSIEhR*g0i%KZrQ340*X z;n%!%HLR^pX#G5EXU)fE2N5aa9(-C+R1n?GOQKGhE&T{vMSZME*yEYVr|E@h#I*|$ zfgM92HdAzAXm2<7urK#{kHbe9HX$+V(&OkM`mhXB>h0$9Oh8n z@|R`AVcoeB&Cbi!*}a)PZsqf~<2i1Qp#Tpfy)1JDYAJqU$lCcX=Kq?adxd_sG1~O|64B4UO zF?(WpnBW}2{r-^9496MFXS>iev4eyWW4u#vJg3$qBxce<#<{}Xu=V}o>?tzt%OK6M z!Q8WPgR9aRIW|uOSV-Z>O1tabH(k14*kGjMqG5HnuHV!XS^DJ(1Rd zlNP}6HF!a-rWz^<{w=k+U3vOx10@74hXIdy?Hd|o79#s>XcS`Al9t5mRYIryXCjD0 zJ~8F_3V)?pREBSHx&SdvSmN&iyRr@5E&~>W7Ql?21~`jMI!Zo*0f%sW)4Uor@Q=R2 zp7~~O5l+1z^&?q&U^|C-`x`}DP`AIJF>2o?x^N7`Rx2ibpmwao(f$7df#2}qj_d2Z z*FB9YE{~bs4fE#npos2jGmdT9#w>S-GBQ)DkUf;3Ksb4}%^hnd1QeJKxPmJr=$r8t z3+tx@s2dV!-ivfqdS}F(gdpZ+<7Q|0E9UO|c0q1;6Z956DtYaS87BMSC}$V8%K0%9 zD0bpe^riEIE*8C*T<;ZG>XB+UHy(v$)}^~gA=JhYkI~wyRN2%9a=EQBHHBj`if4rm zxZJ}CYgabD3(zK3RCDdqA+(dn_cj>qXL@~(=8gxQv?@;wHXMC`#slA>xHx-wjof-i zEMIeMtM;{1$J0Kk-WV(u57d^O7Qn20XgArO#01^eUYfxKy9b-k3TAY_Hgih)t2x}m zTe0_o;O&fAIwHvZcZc7N-=Dq;cSnSc9wI)b|M7}>11%6E*kih-^4Ep_7%hHEv|9zUQ z`%vq5y0XUI_1R3Bkr587$6K-v%g>n`JNGxg02Kgd>nZHgDz7r_+$v5#5#wgDv^NKz z2PN+fCb;M7!QOV|<}J}W7CASpKB%c7{`}I<@iPO`Rv)oceAtCP(EB8UY2aCmegH_l zW5L1Qa{f}`Xl-_e7-mJz z3l-trPcU|tA-LXmB^lyQ70gj9>37GU zDgpNpC#zkkRc2dxL^QRke?XJp++x2gLx!d6CVu_bmw}vN@aWr$HI{x1Ff72V#QPl*f0+l~#Z^3(-R;@yLuI28(^F9?wv zB$~8|aEED+1!|Tmo&1o_zOji|`9$VcM5a%0J;BLM%`J|6v)VvGvrvA;5`xaiN>9B) zho;&6{j77z3lMDE^?FDa1VxewxO{L$uUebZD4DJW004+ ztsZiqq25bobL{H~KRVrNXgKlAODKxa=42ZUZqU+Eqz2*gSlP3KKeJNnF0$NE@}iO> zd)lnp8dQ3cwP&jQypG%_FHb(5`^KXykF#gY?&5X6+PbbY@Scb^JX+$GJf*j0!A3if zhpn_O0x*#L?{W5~Rtwl$>L%WYzYQ+`c12`|>v@h(Ny2buP86gBFqTP!NJYSL3?Sao zoFB%*0k2e`s#@T*5eS|L2X?+JA-BI9USu~E>8*Ie3HS%?%+$woCaRa7$R3(N_F#q` zkmL9-(&R_jbkLs@< zoda8s0Xf5}1rEi)iu2O5>~M5}-D+P&rp@8_iCi|pvhoOiwt?5SsaX_`2&-??9T9Q~ z*Yb(w9+>Q48>62(D1p0FwgP)T*SRtEQRa|_E0)MefbhK$+>b2QR>-hSpN7eLRd#N5 z2U&Vwy@s{@!~{gl_GZeKktU8N+Dgj`(l%0YwhW|Nu^P-wS_&UeaYcTq!&{UlF+veHe?hW0>)AflqUtq@ zr;yXOdBR!_ew*av6FplDqyc_de}Hb5`Ph>9NJIh3JZ#J&qMaPjFdUVphI_Q~R4rtB zphIpW{?$&WpWGd%IlYuV+Q6xu=|onxh_)*uXejRjmSLrve3`UL1!vnzU-8A0qFpsr z?`2-Yl)w-cR7hO=G+k4JxJPKS)yw)#w|=O$7_w2;TrflP=wRAhzI-$n+2vY8`(&+s zoCOepPWbT{Be1&%tQ=aPKcdeyH||-o^2$qBI6FaZ9D6Ma=<7?fpuI0ZxQowuJ9R`fQV&2sVssUWYd)~#?rzK6^gr~V4} zGxcsE?8yJI;JkZ<33Q9O+=ygFU>8vStC;?UvI$qN+0bun9z7S|R_esD% z0{(&h&dX4zx2Sh>UAOTlj<>Jx?$JZLq~ViR(1!vCS(7c~XmZ76gJk&lxle8Ky}oYd zavp0dU4SH={}cQkO|z4qfb;{j1UL^nV-f2=3VAbD!?XK#M$s+!MuSob{?#?diaenj z>AAMv)fAibrUW-3LzU_wSP^8-;4T8A?+1T1AdR2xPqCAKBrnK3l9~~yo1@>^?mIiK zbse9rw14*;uWK;pu{8{J)jdpCkm zTt01&clfU26}-vwjR>TA|1iJgR{{MZ_*hY?2-!PhV@h`Huq9*E{or;tXj2-i;dGVJ zn0k>L&O}&Zcz?2^f#EN$pub{9J$S3V_mP;wAu~gSNY^V*T!R=b3}{s!zwUT7v5{^- z2g5?7LG1q}1fD=3jP7^*&pMNPLoe9LTJldi1G_Io)~afd{J+*!=3zLljAN9x`L7E3 zD;G=OI`Ia7tOozeiT|Q_pQOSQO^GIe4f9Ki^eX|IbdJFcoxL4>(!q);2j)8 z4540OBDhVFCkOmI@mB4b4H-^U?y{fjs^#<20wjWO;mWPZZ{X!=kBEvmx{cZrhDF+= zHY+YT@wV5CIUC$2V#$qAEaG@YVSB(^%6iphT_Dh1#?5K=j)NfDsd^a|hsd{LOzsD% zDD}OvDO76+A`YcmW4RU?(affjohb+WVx?O4$(mB756M1Cwpjh#PXft&(dQ#DB~RmB z2@?}5$mMOct+DS$2$2t>?)nl08?#_a;8}%U=jg@uuT=Cn|K>#Vr1N@;Lk16 z>%sf>e+d1kf4Cm-XY3K;|hnZDIjGPKHPIbT|}Gf^FMZ9M?5# zF3&p$(iO>-Znn^N3P(RpG@&ldzv5!JHnysps^FTag!CK4s>$oKX=4qkf%2ly2hR)6 zXAtF=-}nHF1!b<*iY7PvVXi73G6S>58pzQKFm2Q<5Yw56kVV*8ss6nlX)fp+z7Fh!o)=?I+1$=%4MIvZ`Bf}yUU_v$z>;VF^g20U4m(kH% zC<1U_9tBF2{hT`cm!m3Y`+y-;eI5uo*``69f`QXeqhzFa8&J>zm_QS$-btt&w20l@ z@d7qce3H+Z&Yn>hLuNAoa~HIs4$MY9fIOcA-%o|l2!ZUoGvKp*$Z>W8$NW)%&n#wc z^3f__(4ZASt!~6AWGA!scc^^?R_x6`@sXYUdJu_3fR8l{FJmJ+4s@Kf3nULt1V{o;D>KQlcsZ-x#B;0n9a`U z$Tx8_`*XAdnOp3SQU{6?1G8)A>eg$8WyP&QicCZFOY7k zsYvkZ&Z}9no5b_KS-e;8`R?gzICNcp$Ztj5)BP?G5?BCU24=do+UHEjQD93iX&HH9 zxRdK)Kj-1jyJ`AR-4AluQv z00I95#Y&@G<(&iJ-{J8!oH8PtRS zV=oY;rmlr}vc>G#?y0&nqfxw(O7=-kR;bVMp}GKt0lGJK1-J$hQ<9_1a#wCJabeNy z@o<@LgjwhntjzK0;0W)7Bc!Iv*`qzAck-rSy`<7Um85y!=O2;1R zUaHr997`F0=ns%|^fk?&e8ff)#m91sb2ZWbk4ZniqQ`s=6^Z{%nu`cQiM+ z5|}=0-fG2C5A|{$jPZU|(GbR^o>-`M58*fnHOFmSj&)Z2$zJFmy7C&(m7WM!;%z=t z6HPT4ikDEY;K%4rk(sij_4f>g`pTUfHf2lRJl*T6%KLLT06dbtM7|kCWd)BvL%tXD zGL?kj!r8U^)dA%=@Ol?D3C42A^OZw~4UT`H`W>gS##G9b9d=mA%g9h%p>r0wE&Axr z6eG{IPq@N+e{`Pqo6b$YOsOg^c0TY4p1Y(= zZ!{&Yz{qirDi?)Kh}KwidFZ9$Vf$MQ`9;6;&L*<+?~Q?R`FobTU&V392lXn1hi|SQ zt4qV)MQ{~A`x>Ue)r$8g%eDWW>a?N_Wg|JrtQ`<@}SH_ChcP-P0Z(03i5B|&8DCyjd8|BkBx4GW)qCZ(` z^#RG->}UO|4YZjUpPeVmv6cW;YXUm|4c*<|nGX|-XmKL4<H1m3O+yaU^CcdQ4J zd#I$Oejk^MQ$v56K42)P{-mL}WdY|?PUMj9r^sZ^z!nsjHx_pKp6mm-`;*^ioSI&fu!upBvSm>W}{%L>z%fKx0M2*4N ztq>AX1~w$6R03V%3hz06kyHb}|HB$Uf75LY=dHbG8O9XLAi>G%)phCSCB_gUjgn=G zS&LH_(LZ(jcXBpMc&()w##7X`6M?ed%l$xa-JKRwbe@Kp%FFEsul~V?UH7KIfQKp) zwJy@rhVk%@<$*WDzP@imLqzrs0n8Ii6v0a}=ux?31uj`7RzZb4f^aD=&~E9=HXCxA zyAOL6QqSkJ3H)hZd>8O{TXM-%U$!M3-tUOg#MR2*r{)tXOirf{cRz@5oN8)|GOTUrjd0(Z#{1Q8a%prnLv zS^q{sH8Q{Z=`WwEV}D~Pvo%yVQpUypec@j|EF%F6^Z%wk#B;4jO422~!6TsMX7x)Q zHa_1qpmJ~rmRP`sl9a)svsPeG|4dZEqbQELv5L9#%#VK@q3A@O~-WNMYO%WpFsr{H{`z4hIdw3z6=G-(XU;9 z5_!XLc4J}^5i@aXy4udOmA6Mp^AC)nr9D962fh7_&8Jh3dkunIG*$k1lKKaf!@@1< zfBM1IzNrpQ5sCZ)+$FyEsN)ZMhju zT!DOLmAFfGFWNTrjZ>m|a+-Xm0o`ML*4=6h1D`e>BS8z0>W_Dg?5au7AsuL=sa^ zwbw>J_;!CYG}nNKyR*%2oOU4*?vYhHiFeetEIauWd3?T7=bW#pAEgGG%T{s*Lj zn%UY@hnk&O@tBJ$Zhz}C0SOAtr|BdSCx9O87KoG^GwyCaII{uI(!8hn(a*=-En=et zxt7wfRsl>*#w>;-p>+lJRCSt|98wwf*ZStc>OPkVSbD1)Y;7)8d6O=cmkxx|al}Zv zFm!9^kkIHBBX+fm_gh+&hCV75`|>8l?#aO6IHVRV%!yw@&i)C0|FP8^*0`G!mq(u{ zGHQiAUMT+F`pe{K5(|c;jKrJUf3WC3*pWaTO0L>II}*3aHx{E;FLl`tRT9?IS^PiP zd+WHkwk=(>aECx}iXcUB2=1QX!QBZC2_8I9cmlySxVyUs2?Tct!QC|!7BqM5eYW@R ze%d6yUo^!uNC!mn(XQ8(v!VqLH73f4iXWId$w% zgj6&JJyqogM6lP%MC_lh)7z*M;pK3N!5~b8=y?t0yqco^T$z>MA1=`&@wyWw-D5|X zQRz>Q|7g{MO;?7vuevZkWWm)zyjMJ30Q)S&M4i#2DUxxB2PT8i=DzcngC5jicv*x@ zwfcRc?Y?sKJKi7jRAH$|-yV)1vdpz{jYvD`|Fw_&qd&g?>BbV^pBc(C`{?u`ISH5& z_Z1H31blo9ua-8XM!8L1(<`p{*YDmGDO!7uegmFP|A7`=J|F)Dd68sEQed@QX&0E$ z_%&qedh)LwIO*Z;YP`6)Noc0MLdObj-{?o-S0~_*ID_2=)D*G(d6Bc@KRy0~8WNTi znc<}S_LhqGGb>eicV+zqvwkJ-1Q~b z7nu7@tE1Q7j=C(87Q$%}Z#+!**p;we3_cGG;%h+v_)_^39X8EF8QcH(-oS&6PSu|YjQ;eP5#|g0 zIwqkNe|%xm97o{NDoL$BeyKT0ZrGZCx$^i>%UdR96^;8pSr~(^_yl@@_x~q5=*+tM zf9X8}VCMG=Xjb_}D{OD&xj$cPgvtY)6LWH3mU@EN@d)3^h+2x*W_RU-lgxx>8w@Be0;`x5M6~VH|TS;=euU?Kd z1EgHKf0KU;M2bRles30ZdDuE^QZ&;Jsb=N-BP)HK}W-2wzOkA7?E zWXJn&+SB4(!BS3tlC1pJDjk>nZ~0(@UEJyjH_LtD3>!sMf$II}s z{)}OUL}`qN2>!H@D3h#kcW4yhI?yXr;H~PL-CN7#klio}Ceg-6vT$Ww8bx5xr?pgNu{S=>yS*#~_$@&XAUThmF%M6woqEsTH*0(lwN~ezl=}iW0T@u zNk+hV#(z4YUQ3(2^-92CjE*IpO}+rOEI)N_ZFBGiQu^X$_iUGO#9Q)Ry+VA^%GL9n z8*B>QFWJ5v?f?4YU+>^A{eZpvwR?7bf|$oMW>GkU#}>iGt}^^(8e14=r4se#^SLHBEou1Tk-?(rX2VQt?Avb68V zWehWkb`y?@q{GC}(jORz?R7QZLmCPRvH-7)3Ilq*Np~veL5Z1;WRJ$zlFcZoz*m$0 z%_Zk9x?&q;sBskz97wU|aS76x!L@s{yVV$P7Ga@K!mc5P11enEFot0}pOrn~5xv@J zPZRs5Bg>X9Q#NP53t>#42SCe3vc{N9>bo)H_{%W|UDymB%>`jJf840Y6l;`nXw`d4%$IjU+R3M<_CD*yzW zv^hH4pMIY;o3vZJfI7axPEq@G>lq}h>S+{wM#|R6>geFuWa&A^{ChuyT$N^`Q;yUq zo>UOAuftNowZ^!;31a3`+mQC@HzlWc-qvHimdHYJ{z48sAYoyV09f8@L5}dSNqwVv z1m)atRPC5Vte>H0!iNnfT1CsUuJqYntNKAEq~P^QKVTo*9hbs}qE@QfIw-hFgcgS@kaDfN7DRrN%!^J^3sp|1CNXclee4-ltd z5{;cs)gUyANm`X%f7eGB^a%IE$vk;GWB&w;?$+}MPn+hN5DzcQOM2LyyfNdBY8Q5E zZAG;0Oq)5Sxm3k`%noGLdJ8-D_3+@wE6HUjHLye$YfG%k(&D3Hz+#Lza@+ou~p=Grt^yo%LqdTW}$R9By5%UvVJJkxhHQsB6a&ZeY) z`B@VAy(4n`tKc!Ap&y`KP@JmK&vI^7Z)5v$lC#u-%~@#F8W3v@(Ca#gkm~PqrZ$9i7b8zjtRR$-AxSitF`DEPmvW# zr%l4wxu?|wku;E_p%Pg1@eYt`7svtUSSWSj3L6fXi8t!L*2*5&8z_zgn>Fxjk{2;Z zNy@CIO9XH9!qSy7gV!sr$cr=HLpO|CuAkfoG8rausf?0DSWU~!7dQLktm?-VNyXGE z_R?PRm09pk+D?2?zl+&jPiB5CMZ6&TsP<*n5%>F5NA6My0wbnFJb`%PJwMOX8`4AC zdjHqz$;2Cy{2H$j5e^m7N9I<)Y&V2zDK(Gyy}Pz(<{xTOGz93^4@O4YG<%M<34xMpdh8{ELJ0E1Rilz>_=zx^!cmF?h1@i}klk3juCrh#bc zzV8dGVl3yvyUr*KL2KHZiRu%0Xssdg{q+i2_1^>^)nO-YLq9zmx4Fb&*`W6sav&hV zQ3a7hukYi>TiIoHX`*w4-sEujYwdWc%GzUA)5J)U^;0ZZD9hd9*vM&Wl4y z941RXOgX)2y`AaXew3<_W}%bS_EmSGx*-vj`J=>1@}8bjjhma749RkWDe>dBR`m?a zw?i>Xw8AhK;3CFEfn(H%(Nz*qb_xn6MG4%F$j_9=tTYG=*!As2sfUZj2SmsFNA&-S%3RwyVjlkWVCs zi{D@Yuez#l&vn@}jqCHs{Laer^{}GOJ9f=Q7VN6B_bXV%UAT@he4s~P##kNm7UxWrP40O0uYCUc}_B}w+fJb@9*wulC z8LrpJ#4FNfZ_S_H1eP6wC*mWij3Vq_xLQ@~7oSz<&!d0HK#XWv&MqyovZf@7uW26i zALVKMZ~p9o(H~Y|HNXn%Hyb8~|Cd1ZU$kC&ygxvDME5O{fCp1%>MnXOoc9Cd0==-A zZT|sMZa<%Ua)(s)b-HnX3%vx0MWjMkqU{uSFlbLT;POV^UQD$g7eYTp9stZG z;kqX`dlcU?|9^A(Tx*h^udnA9=zE~LLw|ZBqQ5`-2g33VO}3ua8ihxoJ%^Qyv4`I^ zzW#3Ju;O(kHaoXDu79cSad%O@N+T$VDeR`G~1n&t`F(% zgTJtV@Kq#1__NP~IjGIG4s_h$zMDY>T=3x{5*r3DJZQ|~4c#IKgIP-FpSoEUZy`V9P^2GysDTv7$%AVsagOn?!YWk#lj%B*9o*39#(aV9t+g zhGN@@Z8!zUxdysK(yv1;!g+il`VF#Gf2Lobxk$*Ic_j) zJNtgtXj=Eu#RI-d6Dzt&&KC|0t!CIEjUt;Jj>uux3Rz{T(q>=^M5!0wxS7nGe$gOx z^Gx;GN+AZ7oda32d&lp7h#F65I0KfF7l7`jg#xiMtrg~PPZ)v zr~~W59H=|p&dFR@a#8zC%vT{a$@~P*tWsnbPq$Oozk55XNtS%(>c?~#ns{X3&=k2f z=fpXJD$FU%lLQ&YEOMidmRY7kYXR}F@vMLUD4-S*iwo;aR(lw(?O$YlH~1 zIo}hg-6-Rd=*cJO=;5-V-Hf31T~#BayiuQ?HbJ9_E=aJ9mWZWY3gFuT9Y24osGSbE z2uY*jkKmaPTnT$U!sZ3M2buM0TeFWmg|bnEDyId^0%4YrtPNk`Ye4fH!i9NJ#T60L z6XrMGXd01KwZBwapPVa~Vf8_r^>FOA#mBCZZ&WS4He=RCQ{j@crEN6z5Xm*Q9HcC8 zqc+A(hFoJSyi=zig2psRb@f)o*Gb@!V}!t2!Gev$ykYN4da7JXhNZOcV7tYya~bw# zwj0ceoE~Ng?Bsc}!b*#yhMto&ld7_FMgkg|(Pma(?Is4p9wi+Jj^}l$*wx|J$B%V! z`E%hwD2X-rw_}8H6m5{dmLb9td${y79 zyXdc6)(%r8F2%j}uB$VpDi9PD1eUlSv>za^HYDB2N_gS}kN( z8Gm__8!+nSqMfG!TUbG2?!a8_e=(YUcDW}1eprerqvkWd8y+Sqx%Cr+ykac=7=)S2X_ZqqE+1d6YTxv8>@wB)QMJHuA-LN&5LA zVUfDbMBMbH?sTcoZEYjt23z|tVkT`V=|D&QD6Sr&zPryAOxB*-Gd{y)HPw+CZ61z5 zAVfSz2@*Au=ybLV?en@Bw?*uuc7EL<6`ERr2$T2i4TIvG<=wczgs9zf{1nHj7}{y7 zh_AE{`ZgX$k8n!*Z`I+e$h-jDA4J}NKQyahFR}ssd97jtM5p_taIV*Zs89lz+0)`q z#7j1PycF8lm$o#xEmtrb`@{DXSI0j<5xn2kRwoXR5M%l{hPje}b;>aRSqe=LtRy{& zFVoCuo>24@aNEE@lBXM!-g_@#Q5AjTtY zyN=<%2fld!-EeY!!M7}ycXqwE&sl)DE&z%#Z1{4x!zcZFapYoT=`A4IB208zIBTl| z&lXr{+wp)sbz4;bRF_M|p(>80lpxnUc!n!81XXe{;HyaOq*s0qIORktI#o2gBHG>-x;~Yd z+ua$j?tE27gepPLbau9{JEh`E$ly^0bPmbeol{ewH@v_4GDqL2o%hQVHi`4%}U5%h_eVh@qZGJqD5sEo#Oo^4XSq|^5ZveMiAKZwV{Rf*N0DlK*-~UTd&Xe zEBES{F`)$>g?VM&JZlV=>Be%sk2w9NHs4W{2_GM>$P6!3%?SkrCQEAGyNd=1A`NnB zpt^X;)-fCqiqX9VbK;~%r#|85JJzK6SQqNzFaJT+vS(&ajCeaAMVI;0#+ios6!Y@% ztQ&%dSQ0y9TEKETy@JGm%tkcp;VkvdL3I(vIsaO`Z;hmx?sxyCHJ#uG&tk8b=#zLF zNJeRVo^su=_zYar8zPS;-|%p~Bf;V8TU-X4(x!bV50_)C-XpZ%I%x50&3yhLUa|9d z6#vF+@MYPuMPsOKhS#9O%=KDSe_3XEZI_&?Vz8-pq56FS_g6wen0U@2yvx zU%C$kLE;|vPV!}9p)=;S<=L_{%T0EX7^lSbiAZ!&DSecAVyiGZ8#F|_0v&w9X1|-B zW<49ayQd^6j&xWySgp6v#Z<7Q9>s1{&gl6Roj%+5pSnpYcVWVzRN>buBRb%jlJIvj z#V(@|q@gWkt6gr8eYk^usx`iOdv^hV0O3C+w(V^-I>Oo1Ra*#&QIS-acWL`V8){9f zo#oGi1}=Lf&*?9#it{bBX)sNM@)L5+T_1UZyvbLPQi#j9eC^ zVbzm5&YnOnho-6?vJ!Kou&3`^5Q0j%T}YR8gj)Ks#2bCXBA!5VU{dL$M{D#OlaLEE zg^d;(NcnOG9ZJ|b$!@ZIiOj(h=J3|oHEU8{9Xby|V+YnZGdQ2Xcd6T?{uqc5J_KZ5Ks0U4=FXYyuN>z8*P-Fy18DcB z<@+Yt+lhPtGc%yODY>&D`^^(pp9V};?oVOmGAnY4i_NlZP4%UrPANsuC3k#rTNXi6 zWAJ9{7ZUVhp7ll$0JlxoozZ&9nkuzPsokyi-HDYO)bTdL^Qmk)iL5aI24T9)xEyo0 zcE7HzCcDI;bG z7`=33PND*3UZwMTT?k#=wQXJp;mHn#j#CNS`RtQL1YiOE>3XdPdNNsyu`lfybtH=R=@?Aaorv-)_K$Tcd*~zBlTG!NN<161U~^N_-s}6p#SLWDl>m$1V^XO#)}vs zGxPdE{aDPyWDFN`x$7sTpT8{**^(q^d{ndF7>LoA#d(@uk{aO{U?7Y?`BI$QnYLH4 zvHFW0on+wr0*#IIhxweD#*X!^>kT#JBo~6sQz^Y{ybooiq$m(20mg#$%tq@4Cz$7#LnclGh8M`v5>ms(STHIFP3De|2Mo*F}t!s#`QcRhj>aJNOYt_cUZ@GC7^-``x z#(AOdS>+A&nkI;lx2g73yfTfEp|N1?s7L@o$UH`;G~V07%h(x>Lp$ZGu+CcE_pKeZvV2norsh2f08&7iZ;*r4sj+lI)2(>R%`UMiE% zaVr!b&ggsvQ<0W`x~4ev8=&pJH6S|^y+)-2+I~j-rIANLtyJ)P{N^`G#)J3a z_I|dkE6x<=LfcG}={ewSJ_D65L+ea%MlCxKX;Rg6y~DZp2ZE>p45HtgW0uyRH6#wp z&zn*rBFB8|Q-9RB(2GB1Flp<(=A>CwBB}XSrMfXn8fmk&+>=o*H=~2J#=yFuD(VWl zfe5e%_J9ft?SHk?DgP8ZEo&Z6B>`*BjyQ1CiQOC1mTt18A-vki4%)b!@7)irE6#s4 z^kEB+<`%_hEP0^4C3J17dxe0>U-+d=iC;Te>AUhkj!Hz_1+@}{PtE>Au{uNxW90oK zie2;HM2FPsx@;PJ%(mvN38|{*d`Oyvdc4>#P%<~v8|O4JcAZ^Ax9qvS;+iu08sQun z-zANwa}>)6N^D?OkeEh!ddj+acw5Cb0Qhx-d(Y0~y@x&GiVMm&K#0MFk%k~pq&x&? z8iaT*>|UV7DSWWCsyn@5gGOk``zd3T<-!J@j!BzCEv zdUivVxW)oeKWLC$ME5wuBEDY~*s?P1o7#nLth6_UE=|z8pyCoDQk6e`hIEZDN;R^8 z4ToqMyW5^uJMo#O=DFD-q_}(xKFA4~jq_^Kv65DgHR}^0Ssx9n2juHH{@&Ir)97QN z#&a~yXGCPa>dYCf#KiMGo-mOQ1@ks;HH#gO_U6bxK{jRSpVFHnM&+T=(f9>}Bhm|4 zK0D`RSGgq7G_;3WZJ9^u$#3@hg@A3TJk7SpoO=ljmz7(1s$~+VFF8<@82PqzNF|)( z_#}Iz6*<^s`S4?!8;@SAYl88v8m`~K3@bS#DSHUSy$9nqEF+`@ZrgR`-(P9on?;%O^ktC|V9pTd`Q7*F&rtfIF-*qU=pg^NMGcFBoj zwQDK0GMHg3>^E@$6=*&2@?uWFlXfob&YZ^!CX^yVVMblhN(FJ&Vi(LcKThYz9M3Ud z>u`(+42ztI)f7vc<>pRPFSGzvee(BAZ`Betb#}Fdr0PE?qG5lh$%X!u(Xt-;Zk@QW z{E4B=i=-!E4K@bJpa)l2o9$#`qbL#16B+dSySs&Z>-3$$GJX6T4lnVxHSrdrk!~I5 zd=7*^*-{G~kJ3S6iup&4bc+fUIF-N;^y78RC*b|HlJ$!>qW~UtpUz0V- zhi#bPr!Qdcn!Dw4kplQIf7S_dHGaO)6R@*ZDP2>-k&L-%{S1L~Ee>03yr}`_j@ZmIz1C^h zabZ5cq=XQ?Bc9I~=jm6J)6zqykvaElkFvGs?s0lcF-459u=Fn~c=8N(y3n+4-&7Cw4us7tq5TpCH8ydE_EKRz=e8}!WD~={P4?AWW zvlOL22CnGP{1#Q)o-(RwBhqL}tyzobvN(}V2|MqiPW^7Dj7zve@Ffnb{g*_;qs~y$}&K|AcxY%N^;=*#{ zsNy*MWmE%WW5l|!rIpkYR-JubzO%i!I+Z58m}{9~UEkqcN{uMz2qQKN3rLqnuHyc_ zuy9o*SdeJRi8L2hfaFf)w|~c&)YcpnsG>hVm%mH$9QJNn>eL7l_i7!%S9}|WZx=OR zH8rl2-22e_=n~8BJwH;U@{0?O|9JI1u(^M#kxAA6(fCzze&1{!Tg*gS|=MKK%d zXv&Frcgihn!_R&V$1eQUWXt+i`G0jvo#z%{_Ey5ol>C|q#CE0hV z*lT$X*wTtha{RzRoX&2VZ@Kw^n6Pzf_MQnTSt^1}>WiQ({Vcy7h#W=Ne*UqJ!lVIl zZt?^&Q&0zD+7KLxp!lfa?TIwK{!YLjjbwqAz=X)ne2RiX{)73PD_^P3z9*ejj*KU5eB#^TL0$rMKC6JN!+tQQOj7~hNo_b$MMjU^#HWg`8tJ6!19#RsKm&?~U1 zo3V4MvmV>?#0Rcb5uGOUwE|*!n5mX%RPBOGRy`rO6A~P0gr&4kC4p4pDjt zO#(vU`9a#d7nlz zj5pL#Wk=kHosx42Ju{KF^(HN%dX#IwLEeqX4n1|B@#s-phgAq^ z%<9RcPY7Cb>p8t=9{FshEEC1ou^nc=e<&?bzdo@w_x-(+RkGNre;(A;HV==-*}qWB z86lt#(ZL%_q)S6@zdwc=69dyOw@9;}6_xU=XDJXK+gX2cU)M%+uQ{SFU%yqc0=*Ue z1tL852CF1j`~DrGb!)rVAiA)Jf?Lw=f5rF7xW6oLEc z+Q29bmLW1F$0U4;v1p~z`8(B#TX9=!A?JawxI8m6>~IF!u@M~zx*ByTUP(%hFu-R5 zoTdFKAmYI7Qz#GQ;-v%9DXRgg=Du7@1z}X2AAW~2(gPVQ4VT}-vYX6X5dI{0`=h8$ z`wvO#|D-DUxhxe`3&jZ=;BuC$Lb-~{fOWa-PcjqT;NyD3r9j*vbIA9+dO{p?- z#OddS(#bx?LrzY@N@}_icO8oj_5NRY^tz*;WYZI{SneeDT=Wr%w!OP@a$az|mBM;B z?0H%J*=BmxB;4p#bQPLoccf6E;m@-Kso`6`lMD{0In*_&TARb4=`Yjn9HKmsT$- zRW-Cg35IE$l7j*=e?NY6Aq_xg_OYzw+L zv-vUVlANRXN57@Z^nTPLg*`QYDSW_!n}$G<&c54+1d|r7wyMF%F$wJ#Xpwg2Pm>sHa5h`+ecYTvJI2lONf%O}S~Hvw1b zt`vJ@_bouL%BqvK{WS!EK=UhE{@366(s)u69ITk(O-*|aC@}bp-;E1?f41zJdmdSI zrzS~{c)|ETO0jeB#ZYe&Asd((|Pk7`N1#8loB~C?^Ca zK#}fzNlZr~_HxYdkC8yEmHLmT|AQU-)t>#`Zvwbl-(WI6Ys9ktO+Q46F&_VR?gI8I48MQtqx!YuU78;}3v3}pt0pY;k9RmlZZ~$Pnx<#~Hgt_>rsi>X)c+wDO!p`%pNUMOT;8jv#!e=jn^-NGN z=e2Cw0B4$Vzv3F06#Bk|_Kp2Dqcd}ZvBmelr#O=VBG2DMwSTlS|B%k|n%>j?)ods) zLHi`;{~YyxDYhr;jM~@`L+wB=w>`Pk^se+=uMVB^LBt_YpYE;hu3fq29Jht0$#9*9 zS9<$Ib)Dd$#597FKl2k>;oLAc{yoR9b=Jt&a6IpG$T?)qz4DV~#W3N6426q_dos55 zxdvT?^u{2i=3>U23;xn-W1-0-n@?Cw(>dd#oxk#(6Oi}h*xK^OGw~TCu;a38pky&4 zgI3PDS?bT>A-SDf!CKKTFiVXQUs))_rzfo=Wc9))b43-0HY74ufj=RSE;>jdYefe{ zIO}pvbnq!Ui3?+kio}%zX}+>tIPqr}Y zA#y}_nodqvOyZeE=c>Q@kn+-TY=vS&lIVpAQf0`qr_LFS(otp+@^&NMZKfKcmFut} zlcNoY6tyvrQve^yd^!iT+~(Fzd2sWU_mUHlA`T<_XoHHHimPuHPb}i3U_VVB&EsU| z@Z7i=rRQgtc>(ef{4s9n*7=%4^U2vtXeRr zncNANINr%3B{iMX#NO`O{Svb&Vie5yewldJbz6j)<1Xq(@rx|6_6G=t8*ty)e>+k9 zw?djYcFMnr(-aH-ZAiy2YP3+?o;|30m{Pf08lm6UIqVQ+Y%1D-X0jjsEf#68U?RCJ z`PKbH4YSG5J$YOcN%5F6#inWN{YW)wxt50>x9gsH-Sx3#_d8Btn{Z@mK1Y zC@YvpBgod;tgqFz1nqNV?$s9OnfmlS^4{?q(O^=jw7xzk$*rF#Cp<#4%W1MyA)JM< zMSW04k6Gp;K@n6$71_#QJYs6=l@{kC5k@k$NoapL#hRnXDO6_X&+n8e(cMNyv*CH0 zna%*2EZ5U`C~qHDQ(8_)84)ByF5-O4Kk748mU(0=>4KS085zs$W8&ap$bToPpg*^Z zhw<{oqx9(K3CGwcmH1J9Z~KJ#gd#k46>G+Oz6Z=buk!HAjd+3j6vozv`^^Kw4?a;G zY%CuM4ef217~<$;Yf$0MV2JJ{3nl+fa<=JSh9x4(IDmQ^39*6Ov|p^57v9yjFiXTB zICuaxQ+Un#IM(}-m~;{IfEjpcn_`+jN0 zHAyM)*`37E#o^-yf7C}83`;5y`vDqDbe`}ortn$v?}ee76!KwTZY8sKPd`xK4<)O> zq9(Huz4uzQR}OEwGsFr44B%(y2@vPS(7>y36a$rzvATyWF>=fkWRJDSC!|;?@ucS8 z&GE!M%vYi-dws5{iJePf0@P5+u!0`XXg} z;D?btMIU4P-@8k^kMnEo8!IwFPQKrgTpm`CALHU~nkWbhmd3SAa|SQ2O_#+PI`|!0&gN^LrC8 zZXMzqvOqOnH6P$-_xK;$Q)epwdmjdV;p*M7P-Nu>GDg@+M29DE0mW(1uRg{9=HKvq zBC7z&y!{NKs_x+3b$lM=tW|Gk)U~}yU(uJkOZ}^#fS&y;DZ`m;)e$V(; zM)_5l?%S8kj8_-svoSL(6zZa{e}I%er4+Js&&7?;m-R=#cuGcd0|^hY({aO$Krx-D zn6Y;C^ie2959`9*P7n>*DnnG>{N&pW(gQ~cKsi4smG33ojuq3>p$ zSV;GvvsYwnZLXVOc77E;8$HJ8k9@+If20kM?F84G?@?C7l9})jGpgr=y_^=X%E9uKMFOtVYUee922 zFAEh$vhct9=ts*Tbv*Yl4>L8U30mUyMiEvLW|F_*=>0j#{KnDZ=9PSdNMoN<;@tlr%Y7oiRum-p?0BTwb;3r)i0R zeDv09@P74P~oebIAKv$BSQ*6={H9A9$+USID3j>&QY1X*I--!`Ss1)cJ1; z%9obDw_~T-VutR~SgOTa)4D>aIG)fh;HfZJ4FlXZav0#Z%+t|NWU z876JRdJHOJItN0G>{i4_7S{P6#Z%9A)Wh`N{Qx=B9`?yX(sO3K(6uxlHEvB$gHH+^e$;_X1LNmH$(aEg}qkMmSS_`CV44;(s9-xFWcry# zz@_!EYlz;9fvZ@-!<7gQMLyd1B{+IvlXZlxi)|E4m)4B8H`QgSmJv885k7U#_7X*{ z{9S#SRX;sv52@C#$y8l<;dyh;U4~T31L%rl?Ql)^lm?nQ?i930g`+Tq@~Ps=F~cOn z0)nNAst zcRIcnEQ6N(qxwg}-d4>R94tKd@|{o8UVX#wL@e>`xVF>aU!R=FB8{J9X~|2KWYv4H z<%G?XFo@OaAZS@;P*)o|hKm~)5J8i)Org-d{;hCz(l#sFklR7yJ!6u7aVqYoSih+= zd~1j4&&Xd`spHLNNEab#8%X!|(I-7b883PbJ)L&y-Y4lm2$9U)&|3zxDdHnF#h`Cf z5vDY@CvA=FcG`A*cptfeD}`Wa2kzNe0!#2gLeunaPm(y>CjI#qe(LO*Q==(*FWtL5 z3|*(4DHlFRKaI|aN9{!+ffN|#m)p1}ji6M><@B?D7^LYuoHIGAI1=x;fC5WR*-xnk( z+ardM>RK?rJ6%7tMc=p3;$ZLr=?s)ge?eUEnu#NCsrp-6pRKMYbEnLbIEJs%K&dnx zxu8oKG0kaagh5!!6wg;;ru1&lDV+SdTIJ7G(JzR@dLa<{<5PGX;j67O>Oe`A7<^;1 z7$|3dUf$cnS`|sE|C74T|L=NU&4Dm+^jbY9eFh5>bR6g?!bxeG?vmOo^Wd$gPRJSo zD|dXm88&mRZx0EpyHPO*VX-q8qUlVDviA1MEFWtt(l=~XzNa&3yCJ{URoH2-mQ})o zR~6sss`!4-)j;Hokx?`EDQ4gSkmYEm7>&{ij-OAsMS~nczGZUBgViD|#Fv%^(O3FS zhP_RTqcwZejb!OAeh@rgjPPCuo;1iGwf}f2Yd%G62SL@(T*l%t9VN1IUOaKwAu}@P z`F?L12a8RQY8I`13h|I4iZD>M$LrB{X!u!?L7!LVxL|H|Mf@peAvho%SsiSAXGJPC zqk%Zf7*(;jzZv80bstw-X`_H$X3a-S7u}}BhVvLUxR*o0t1>BUja^{@dc3*d3_}IC z6OU+-kW1U0%dtG#dPz@sHvGYGS`qr)NSn@R1#}{1lQ6Hu>&7by{n?NVS+GhR+qtR~u~*nR z0n4uow5ReDM2c^U=i)KybXfdlq;H<%h(?uM{)&LO?r{B=R&CE8{O4C~06`S=KYfe9 zX@oA@-=R*${{~MK(f`%Mg8#FdQ&9x%6WG6RY=?21{O`*>H}i)qOm>c(0k?AO-yddd zF_xFn&F1;_|m^+wDElupHBqLuDWkrv-%a-U5KhANKoMaNYj< znR5QPW2OFo{(@Kwl+d!D@9U=vMr_{;Z(OP}2^n<#dXJd~;;Bh!y(JGN{nsl{j@AL} z^rzgRni=;JY|yVHfTI0%B7=)FRxJ%@67J{?V<~a$VUI|zCg6Pjs<{3ybjShcJ(Hyi zZ5mnhQn@`-zm1KA5!ZkS|B|_v7TRNE9u`TlJ~PT75p}u*`-{8w-sgIm={nW(xsnos z#+J7Vnza*|wGJn(fq}E9xRdC@qBDRV2dPNs{By{LTkunx%3kU|p2#*u&IIDavu$oq zvKl6-GFu_@u7@81A{;Rd9{=d0I@F1Hcn>122k{})0|XmSB%BJ;m9~{?CUa_VnN?*7 z*}Ck+G?|@bL2wz*O^fmmNE83r84muVxaa?t+pdcS{ZsR3dC5MM{%i8SC$ zM1WW>lI(U57kN^6c4&bmvAP<{5SM*qVa&$g!-2Mai=~rrKS3A$XpM@-Mkm&R8ALK7 zFzH}Dn4o3j&s z;_%4wfmDoJ7=R+E|G|0bt1+q*FAUmmudSEABa8M#9<)HO&L-QxVM4_8zm8^AhX1ts1B8~t(W=+> zT6u_l47`3ts%v?(JYb7 zjmcZQcdp)91FJHTRx=&PhF%Y(eHd~BU)O4C2AN-o^B&nzEjKVx2C{l=pnktqso>YO`BBka zoa2+;WmukX?~AaXU@_y#SGVuPYb0kvvkb&3v3<``C?dJ_v?{&cf7lFNG*qINnO1}g ziQuVMg0w@TlXdmExeH4_GLC1x43<}_`m~p2JaD&yt+z?z93|{tta;7(sj^y+ap1k3 zb97h0P1o0Q_Sgl_$?8UR)1f$Zq3w^AY zx(A%|&CS)_g%f7DzOB(;k*aNU&R{3{^bA7lYICU_;pv=M<4l*wNI>hl|;7d%}!=8n9%jB+G(wMp}(cHhLNx^6# z7bXaDwzzO?s(TjHHu>H~1-3fH&_Ryu1k7a?<@&o-eF=nX1Tjk}SMgQhS;UwTx6uIG zQ7Uwc^{k{m@L58^rTG1bDejxMUJuah#CjHNi_FIjVl@93dv5_%)w(VWFQgGE>4rsj zcO$W+yQM)uIusBVprCXKQqtWZEec3?N|(|tEw%in?z6Y-bM*Z8-ZAd~eRo_L%9xY+ z&UZfVlW#n6!Ih{-FWA-Xau#;HiR9$%tx>s4U@fDy))+#-(Do7$4&*jJBYC;S)~g)P zs+X4}8~`&<6MS3}mL~v2G!zRUpu2UE1+wMjl7!1-_$;ru;!>yVx3%LGZojxu{%JZuaGYO!BRNTBx)=Fb*qLv7(%QvOWa8-Gc=*V{Xs9iC%dwWtoq}$lE7tAC|OP4q?G_9d4o`pJ`TyTjreM z==aqW;bwZGKQ(ufZURMlxiq4&?TE}74xAe&RdjgBBS0I?^7e3C!<>*f2S-Ag1N)Ph zZz@+uOxbH4?J<2)cmhT*d@78gi$G(7`2v0`wVJO0u~&34U@Dh@?-Q@3JDmL71P%0& zTfeX^;A%+^&)eUhGu!y`p3-af1c(hq?*ipK#)k(7+3>ApWZWTR!zD5$S)D8W#IIv3 zO$j^U(<4{l0l%)h(}>6PRZRi=uS^e7rGw^3*f8_JPj)7zb0ajr`v}bZ!$&{|zd|qk z#KU=yRx|MZM;GqFbka|+OL-=^=RNssW0knN@kXAW36Q3`wjhVOrn$=c7gYX}%uDrz zbGMP4KF>%nPZ#Vw;Ei=OQD5z_VZp7szrJ%&p+iG^Kk*Ivz6IQ#InsSxX!aFm}8?3^agG^PV&~8OCO%Y zpG}D4M^o0;Nn7+|KeEuZ)Rp~I{LqBHN8lLPE8t7A=n+d!)VDNuK5!-f?6D>sL=a|9 zs!>^2+JDY0bQ8kLx|dZQ#Dl()9tm$fOZH4MW!xwpP;~dT;)erskF6rqu!yD?FDv%v zd~EbE))W+DG{cwV9`J-$%L=93zVt8VuXr>vJtq;?HzKw16{PqOe`g&DId}7I;Z*SS za66EO*s|M@O@c{Qc2__^JP`k85>Gd+{5+3GtNe_nc|nVK7x7y0Q6hDad(!wPNGfZD zf)tJ{klm}!%yqyZdeAGyrgFQ6Upw?64?kj#%WbZJY-A!4C?;9W{?IpV!Lz%kJubg= zkdruzKZAu3NZ7p`sWzJGrCBpt(r0xC0o2!c9v}(RN-nW6JJ+DBiO|L$p?;0n^*Fq4 zaH(G{O}@@kS_!W;*VsDBl$D5!b8v8!GD34L!V3*YiPHWRE8g8cu!5I)sFg*FOWWB~ z285*=Az!>W+PJat&(f?ZA5Cfxk}aZF29_`CX|MxD__zeJqYT#zZ}xT-XC7L!+|HGX zK^{Az2zTAdHWYthRUNHLhs~;uy+YQuhM_znN#Xp)hdDEKnj}ywl{bnV?6gdwwjm8c z?3AJIC2j~pn+@;Lgvmv#&EJ!;BuExCdBuerC61?<}`sB%Zh<)RhGm+b0qzX#1j&V9y??>yUx<4lO zjJC2YKth0s^nklv_w}equzj9^WrUhYt}NkP`Cc=P$I7G3E;c{|A*)u@r)l2Ksh^FZ z>sB*^w#NpuJ5rH6fh5;9KN|W2Gi_X2(KQV8WnW4`g6AR|q<1u)bamh3mWc!OQ@m!NwEQGu zK!k9t|9O&+tC|hYP4B0Cio*m!GC^8T9hf>jZamJ4y!Wm;Cx6DXRnZNySj2%+jlVK%??`;e za?nkd7(QpAQ6-hHS%AhRX6C!=;+#}@F3mzUG8-BcRI2u5(cw}jVoVRQf0LPA zp~ldcH5FEQ5b~Rt~plT?!}a znK>OekQ5>VrwQT!HIu)n27k*e@FNP9_D@f7+q?g=^>KaMplH+h00VG2E@i(A*Zprx z^ITFi<*#!qk+$d)T*MDO!1{-ea$al~2uDQ%(H?)afer%EOC%>6Sx_4N z3K{_sXpkK&K8chWEWd)}v*W9wuk{CeBIxwkR2NDr|%lS3#;FS zo{zyH{xtmZ=cl0YJpXZLjMALKmfR|{MY6I>gCXLq!eT_^KT(A)mBTSfe;52wqps&?UX5itvb4cu^ zt3UhPc6>P(A&^X~){if&zywn)^gJZM-oPzB%{s(tenG@k$GNAex4W6LOa#VDBcup5 z_4X_m=~fJ1N%I8_J}~iNavRNnTrL_rz34%b<~qR2*ymTYMrZVmR_d#J~5!4SXp zv>Y;-cYxe@R-}ILuu;h_7?UuDwWmG`2MGz&u$*+MM9*82wX&6G1X3MW`zj+C8A$Ey zag?8S*Z)(H=m91jJ1Ze}bWfC*JE9CRQnr&V+jMZy{ha4Q>GBIUOujA<%G3-WTjm{e z{8|c&AnSM@7hS}*N3@cHVufB7`y+Yw4BWx`2g?JT!M;kZBya3HWx+X|Rq|m<#Iv*c z(RaIp47*V2QzErRImg4{)w1+u^tcfY&h%`^uq9+>ds7Y48oXS0rt_4PXzF$2QENoP ziTk9sS<+*K)+*M>@{O1fBUa$MLj##+Bj8vbEnQ`uYdvzCc{--8^pO=Dy)576|IP}* zZ4%|8?P>{z6fmYFtx*@SDQ_Mi_1si=jB#MCg*4@G$a!l=InB7H)_6gV#$}hu>|wZk zGs1y^<37f-rt#ugo8dthdajN>iST+3(m zk&415Ns!9Eml|@CU{FJ--!f88MC9zvj_(j-qYQh#U@g^Inj4p}` zviEB!p3%&(L`dbo;YFeR%Es6rh}YWN+aHS!Az24qLl9&QaKBl)54lZgBDqw&@T&k8 z{|H?yC%zkE`{IwTMA?*bBzepIQzbH9RiX?Sdw4^ufRkFPWPN$ap-C!r8aJ3arnfo) zJ7{S)4eD@A_2C6)*z|00Wri8mLopXg3O>;L)NHrBdp4@~+Oi0h(u01e{9_F0K(_n851wu7Qt0IH$b>jX9Os4mG(n{#=xk5QSrFwb+Dm#Jr<; zUkv2A{lJ%AYe#wz=12Gto;o`-RyqBu(LCKe$Ar?=vmtxK<5nyc>E`Y$2H4b7oI&&r z45K^6SvHI*Ven}mMGHN*8)qWLm=jBunef8AF6%<{B zXRHq7rg5LRa)vJ3wu4jwWBfh-C-jeaAm(_3^1l_=yk1WseDPZit(RB8#`;u|)mPA! z)IXv{N(wF+6wLl94NcmVtT?yo+|q^ApZRJ85?f%jgqNs;hj{7kEC8a=e|Ajr-BUbv z3i5}`ch`@*(QkBgW zMASmGY%+9X_*+ni8pN5@u_Ab0wN5oV28nxglS%)NSx&yph)+Su=Zg0&+`Y~i)f0OsrsE+v-%_AMG9CH*r+*$(@?T|`|H^y+SDDOZlP(EKlTv`{r6@mq zblL;|?=d3ntokai6%a=kr82TSV7uE`D(Pp=$JEIub%f|ZumD>9NjoV7cYSW6$Ex#F zdgOS4&r5kA?9B|ihzht`+DVi***=4LHCD|d%=ysAHxxBsAt%0FSZ=0L3miM>v;Roi!R+~9M>Xf|>-SHW zi#oB(f$zcnv@=f@`yb?ov7q;iDOA%(0-687!i4QIEdJp_$ zCE&xH^8_| z3N2*oN%zs(6Qf(WX9>E4Z(iFKB_!K~BcQ`OCoC}N>=RxwQynGOqw$V$^UpWea>SAl z5iS!6Sz!01><>x;*`*kH@Ht1`OXqikXP($mJ&Z1WufvT#Cox8bQk(ThG)CE9d-^ubab+;z&%rs|W&PZeOXB&ayA#ZylBN zsSSX0MD6Q7;s1QZ{_Xz8+elOtm8*0vhd7kY^jdwKtc3Tx9^Ra(qfP$0PhbkIf| zNM(l>`Q!NouJJX`L7p+v|9?L%-bgQ7(Sra9X7s`abLW^WVVk|HdrffANDU zfAj2b0sF1Ge(TwPwP5_7+2B$Ct6y0qClCnEPY6yy>lYU8CQx_#=k^RJwEYX~^{uk@ zHEVWVO?@k-{X=VAH>L1us~zsU>uPdE{2*Tot&kvL-7Kd?Zh?#9UBj|bnx)SL`G3w8 z@K`?Y#U*=i@5s3Jux(?0okFv8uyA#Au{5>+cI9Yhi^jz*NXbF@?Mg(1U7ec)jhh3w zS7%q}=`CG$(fTQ7V=H}^S$qunMwYEfKmv(&M=%V3dYHrDX$I`>b+)`am3XNUI z#=`ABr64yq8oRuujrDyuN*)1DG9!Dhm2d zw3`^W&@phxadEJ*acD_M@yS`}+1QxrnVC5GWraC;qlt;0S6C%0|6cp5di^s>H|CnAz&loP;p8k-BvS2 zrgp~V@{i6y0ZUc1;HeLPrr|bo2|z`|CmMC+6q3FpBP%BlxvQb6 zrLCi@r*CdyX=Q!i#@5x%-Q(dSPp>CW1B0GD4-ScmeGwP`G9fW3Gb=kMH!r`Su<~_P zbxmzueZ%|Kw)T$BuI`?Z(XkKX6O&WZA3rTEudJ@EZ*1-#93CB?oPIew|28f-5d0s* z0)GE=T-d<4ZXh7QBOrep7u=19-v*A2fJnuOgd?ejZ0dZQn#&&rS1LNAq6HPqt^OI$ z%w-r2pN8in?f$o+eIMC>Y+wQZrIGzLupi@^2BE{l0mOsH28n}aBUY@;R-_9=nBCwv zbigr`f;E0u=6F$0bD}M`{D2Hk`$@eYR%#Wx;%zKlbOmL*;hZBdB?mv*1+XU-=FK)) z&C%;rOb}&I9BcvJa=w8)vyy<*95B~DUDuWkr>v{Mp$ZF(j>02kK|XzU`t~BCI_K6K z#Y$k0BL2Ry|HPzslE>o#QSBg)VcxNqJ6O=clBTP3T%SzLvt!__s!1xn)_<1}B|3rB zqHql~npJC4N*jXv5L@2d3U%E%iKR{97wX@*MDtTgXsFIh}39whZYp zLkP2Y5)#%6&&|Thl5BBsdIklPyH>3UAehr&+Ol*QT->2J`~z-^`%hp#z`|f}C|`fx z(j*++KavtpkmpNrUW_Thi>@~7Rds>PK55urACa4;1Uw84pch9>2(^iaHAIhyz@`X3 za<*EviLqYqSDat9C^Nji;aK$-KGgmKHAF$rJV;QF#KJF1Dh0D(U1(p;eEOLiV%g6c zm#VTB$8w5;GeOAeow%$1jVnR4o}}+9qY8#_1?EEfI(J?l>d;$iz`Kg8s?fB8H~ zJ;zgENAbV{I~d&o>&QNj;uXG$?oY=}6QJA7b!Qz@sYljk)S&upLoZVUL6mHv22mHi zcn-eF&rq%9yrWJ-n~{3Y<9+e~(xVKc=gEJENcOngE;hhj=i2+}$8*vHvKltSuDa+M| zIzEy3jdWLNwx@D<(%>K~nu`KnELO=bWwCUt>wKgD&nf*y$RB;Eh3X+_+}&5I!9HY? zs}DYckAzjrM)eG{1fk*?Q1tM&ILu0wwyZ> z`0swkf2E@Noj{r7AEW=l%!PF)S>$7Ib~_xhd62#X=zRA*`UmFEP%HW$w$xWL8+^+DA+yDHUBE{jxcaf*C0Yr!Oo9O)h+W#Av z^!-M+@2c<*x_#GDHQ%{J?*gFPKUCf~x_wu9g_Ga8#QGbT{GopVF8M?M7MXwNlC67ZkLq-L~;E8pbyPB%RLsV#_nwZMEjuUZ)AW79?gkf(V( z>4U$;^G*YAY+$b`^3o=|O{mpGd=bd{9IjE95D}Ng)BeG~(1wyfAYeDmtQJx1WLXtC zk3K{SOlDF6q}$2@wx=TarmUWQVC{rS@mh{5%BWNs_Qc?7n;pai$MQy@51)Te+h#*45HD zjA%#-0*2NCn?B)2H(i7}39f+1{{3RnG;ZY?;*k<2n<67_63Lnwdz|(MC*#8_eo?Cs z!!QTO*4n1>5>LAON{y;g2Th4HeGgY(@@yNY^a@7w>CVJ z$}TXUT#R63nIG?1Wf&cZ%36ej_d}+qeSWjELS*dCFM8yOiCL#F={eD$EsJ&}ifg0O zqfhQ}N532GA9WMQb7|yNuFwo!9FdMhw7ew=2As*;M_VZu#|1S%VB)b)gL!rW$3&x- zHWydVT;l2jW)v%RwRyeu_$`a`9=t>D!?hPYHPvP{AZW0gb+8IxBpZAoA=TeZ=Gh?& zM~}xg^%b<10zJJF?+6D1GX6xS@L+iet$EUdf?c3FjanVr&>O6_0%T#Qx0vcoebGH0 z&)b-|5AkKKz>`5BU)h`)@yo+V7*H7V%Ww_!Y>nbOZS$RD*WqdJT;Nu`!6jLG{FL?7 zK;V4+Y%l)1BZmL7t@Str@nw^gA$CDfVDnDM%RX`uTGu*(|anWp;KPb5U zK_QV@7v)?{oQ0+y)p=Z{LZX6=E4tawm1pWKk5UCPZcp zPe;EUww9Fq;4A2GK>Pxh2DnB2HP}~cGv}}b?BdU@PIpYwj#gqVw8KJQ+mO}_*PYVD z9vAkN%Vy4lXBkhvJv`g#?Uoq|e=;AuQ*nN&*K+Z6#h4YKApuQ0K72!K( zctmHRU~cSfX^=Qrok-+)3@@W z&v{CV5FD2Y|M#7?T^W4Qz4j!LE)a2jpJDS`B~>nlbQ_6j-49h>T6-8i>?A7i_&JyA z`#i~?&6ZXUU87}dd-^dM4Lb;O9z9+(BX?VGK_vR}RLr`c#RW&lQ-I285=@F#{ZoKc z7Pbi(@AkWzx>Wj8_YBDpvleyaXmu!$(=3q<2x(441oI9*^ENcP6V-vBR71%j_`n`y zQo{{+9AErO7QL?mmI+)Wy-cFxKWd|*$(CWa<$*k(B6f9B;_nQ4#rB-!?ffNd_kSRK ztgipfB$DU7G2q|TH4*xoJ~E8ih8HQb1^)T|oPkJx+939aL|$tE__)>oA!1p8R~=AW z;Je^;vo&+~oMZuDZl`_Am-$@}@~K>P{h=OzzyB8n{ z&x1PH@^{@I0r7xK{r&WR0yc0W*?KmbCV}!GHWzH-|4;-uQ=D>gK?LC)odnOxKZHIWrm1|cTV?uO$}4)h8rFh&1-}EF#j*gIPu-rW zJUt#Q8N7N6J#ly7@&Os-9vsyfi~%dn^(?j71U9s~hG3|NUl3-Ll%E%SwSF8;5;tkh zkcb?iil~I46K4>zf4lSvO4B+8umJT3v}|bR$TpZ;?NL@`2;mw;SAN?A33LM&Bi)Pm z?I-?&?9i)%HNatuNzD|+WMOK&Ez+PED6M>Skx#tOXEZG-FmH%or$w1s$Mim@LrK45 z%&K5a-2z#+W;8(3Bv?c)`B}8{n)4`f`r74TO82dptrKLG=p-UGD*Iv%bJ`A_1@fkW zDrm({(>e(<3<+3VlLGlN%4d{gTk{N9DpHC_N+!kX*klx*&gfhwPFC<_zKVT=!{BFi z$&lAsx61-rj==E3xrJBkAu`O!7a2ELPN62hof?feYIJEEgD=ku)xhLPGe2iftT zbW5^yP)b+|;Vq@z2ZT@PbBsyDA|y*6J@Vb;4fcnSA&rE((-0wI-L6OS%2RYrvt6iu zDrtu`sLLABklc_X&_JZTc8%t0oF+$MNMI8I6~EJhx_0KTb2%AMh3Ew?fIB!R~e zn?;y|z_v6tvejE;q4I z4g$!XCj>>snP%GgAomoWhPVZ{Abv>Tci@aZUOVD7ayL+U9lRFDnneS$j@F5RgD*oy z&REUZM=O%VfgM4bsIqEf3>is`h2dJJJBjv=3SDem=6X+hEoq4s9;mR4msAyE$df!{ z916pd%;t6+eCA}#%a1>e^fAUc#m9MYBsHuwBl-z5dH{wbUW}WK=ITQ4a~gRwrIiZG z=9TB&gsR+Q%i~p8#aMDUSCeB5mxvtThE!o)VjBp`yC*cU%^$=3Kn&qt|*cDurs4v!PQS1%G4r0Et2Ke zQ1x0)$5h>PpgNW(s7re$ z6^c2K+7K8EZkRWSyMwV!Ysi%P^sxD~c>M_S$qLQPE#8?Wc{1jW#q5p!QH1?wfV7_MRP3#p9JoAFH+ z5#GTcYv+jNdk*}|`4!jpFAH-Mvj*+mM*CL|XqpdG)!zg_Z=vmgpd@7t7dNYDS8m?-;SJq&ztVGht4w zQu*pdhHLJ)5`8k2g zi-PLigL4}ZR|UiCFF6Wl!0$ghhR7yV;M@qwB5XkxmJ=jI_Ujm<2oHQpRurr^zl{3a^{;)xqC54 z+~iAD9(;?$4iUXOB8hUiq-PxlA!S<>M6k}ajmWb%ac8t>dVQh%Ma^8?%=2Wi#uG(i zQ+Ty-<6I<+I!bQ!&b$HH>Fs8~sH1SOjPyb2EOuOn>sBY;@TUaycEY?*VXE+)vW#D{a6x@) zA-)XlAZ}c+l&>I7;Ihz~3&K%bgJubK<@TAJ7#lyLUcrq@d7(A=P7T;&29^_k zfM4}1^6NJi^9C=Y3hHAXxC_3A-N^9v?xoSAP8@y65avSoP(bwUR?}w00)8I-1Rjj+ z)*u70lmV6^%~04tJDXNm=tP0>Qw^zsWsqtrD+i|o?pm%w(JI51U( z19q~6M6Ta>n@Z9wVpNlKvV@?T6FN3L*&_y%Jk8Eze*s-vhAxVK1zF4exYP_orhkdx zoidWs7r%|r*691T-FCgIG%=|-`8AuT9L!e^2LBaw;`Zaxof70HM>$+m!3REwyu6IO zLpw0xN-bkd?QJu1!OpCwYCbAoG+ov+tYpU=TV%zZa~KTP5iObGJ?^fm7g$@A zD*ZtD@Q=$e{m5r4l^S%>-c-@DzJtuWb4Pf_@?#TfvU0|3pTys#jd`mPzkFOK%d0WJ zV%zuT$D7wXNxnbL(VoD>(95}_pM-t()3M;525>*$k;c@Er2o958hSK9_p}8zYt1B< zne~dq8!i5AsPA>U{*A~i_H{b~b728O*qe+Owte!yCP`TVRuPd6hkww~;Jkjv{;e&2 zBgliN8MRSy?gD2jlxX8|^y-GWymm~^t3njlIN&-rRFt^i+rK4;-4viiCNZ<%L2Yp3-_=abLSu~ zB`aJ09}ljzNhca)wse!rmxS>&ZBQJ31kyP_#@x@o@q+=b@q*j6CRtZt*kiV)J%1Pv zDD{^}V?UR>{9*)MfWN%|6(kSH&&{%nMQAvP{s&85<4y$_AYNhw#;47|#-;d(_}cp| z*cXZ=3DlobeB>)A8#)*`arGI0e>hQ6I z><3Ko=6*s+&6`9&Z81~^4r{k_rUpi!5jpL_`&h^O-^e>|%_rtQm{cS?_w;SUhc4_#!aRdFg0*~^plOk=+vv<{;Z{bT(+q5D65o{$t+EuT6E1 zHIVwp+_->e(vjV~j)OrfRii;x{JjADLEe3MKFnj4tE3A5Se3EdJ08KUF*(XydC|vZ z((vT>tAOpM0GChtcUiYR`1E<-rZ3R%8M?-kV!kh+OQXv#^BTa*5vML+f0CZrdhan} zgVEB(rr@RA7j+dVZAe_zqa>Nkv19QLN3EvQ@`F;`g24%RJ zpJ$-#P4cuWuM8hyuG`Gmd#rgXy*#m~Ld%CFx(oo*u z_pA2%y8_69o#N8x+H*Oj8Sn%Vd{6xk2`Jb^xB7Klt7@*4QEFCS!~=!<9`#x3)Y z>rdYDCH@zp^(4^QdoI3tXCD|!z1vE)1|RJVZJvD4vl6p!yvt?42DjU`=&J>FthQJ{ z3ewHFhc4$hn)1XbX7ZR}CQW_sL^G^CRg1-IA3_(=_ta6{_XS-U81vW}W)&74#vjUv%o`orQxA3FM*AG7s#Fefap|WnI65SPdzfU6bZ!cJ$@UQ}a3o zDB6Z+r;Mw@`c4iWZ%wR-cAgy<5hF#@UUem4RzY61&r&dA77VgL@4hZI?l=u44M9-j zmZNv!LKZk$J1TL+yvO=3XhaZkM=N%Qc?br^8zr@<7bH-^z%0^uG5hX#)xp5#WM7g7 zvaPhZ0)ER*`gJEMJpHyk@Y~!U>~+po|=tZ0U4$wgfT6fz;wj1^3K&=FE;x)h9e6A=1@Y zmS=53F*f>!wU}Vp&gHOT(;4R@1V0sx?wxy&^HeAJA(pP9A4ZfYqPaUO72I117HS1Z zsYp|nce-bphuw?#-;OTJOI%3Z7~&Ln|11Gsh5V;3&wQP*%|o-ar^ktndW zLh7u$r5PjLIqU{UC;fdZ?Dw)g{6Id~QOB{B;xcw`$F1|pZJyBX)YVTn>oF6yM_Nfr z$;{^>0a~id|!-v6mUwve~LB?pAVxWK?2 zv%hp^%l$%AR+E(r-JD2caDFYX;;3k7Gs&k-iG^T%q$2`s4T%e=7eJF2b&(@K9<(La z&QD^Z44SqMcyAyVPGjDP^+wNE88UO~bDrYr$uqNIm2}|kjgTsdV1~29VhVPdsnk;5 zOIV6PAw9QAC*~IlF;L+W3Xd{K4mS4S3rj)w-f{3Le3w=_({E!>doeRPYxqVsN@R(p zr`@T(W7h}St4OqQLfmE!J#C{l6uXCb0NS zCtrwkkOi4ig_qK#_&4BpoDw-6+fU4V1{didIr3HKlw=y!y<(Z^+){8w&PgQEnYDV4hDr#$>6|FRU zkq&IPUAywo@Lp1ykmAVYlcrWH54N6qgr6jPE8Nrr$I&XtTRGBBg29~jiR#iP8mF?5 zK}?0vj12nci@NGsRSoVa7HRhtXY$SR9a7E*6txDPHJkS=4qv+23dIvG`q+W>hG^e39exU?50b}6rRE>i z_rt9KT%JvyAv-Vnhk1|5Nj7mCdP1#27Q7Wm>8zqHpd4b#%Y7z0Eogm>YebuVQ$}_Q z?*1>;Uz<9$Q&GE%*;y0b^Ar><%zg8T(9P_KPlYiNMJx*=Ed>pr7-9+Xn&|d>o)kD&%pY*QGod_l<^tS}I4O)f zBAN}$Ka{W&nHcoaH=>Yz3|N-ebnaT0Uv~8$kG=4gzYwg0eW*BqKeJM)${m^z(`!^eWS1x>@Wvy1V1j!`&Og0 zbr+&Z21KdT6Q?E2kQ+qk(M`S7VQWmC%qiIYV(AWFLG~NE&%c6frpz9A8gHN;E-%`z zrAm@&634&6cvUbnJ-9-{#b{$4V-mndSn9e}@g^cJgeUx-_Pf_f%r?GLwiPO_;3X5Q zSYfFSgr`VfLFV-2)t=kzmh~6zjo1(#9cx=7VVQ(+zA!AQicP&|WE*hhNpYc7cVEVD zkFu@%#X+0{Tm+X*Zg*y6PH2=jN}SZzjApp_XU6H`!_zoGns<4AH*R$>k-05v?hV3k~PW$ z7Itpmn~_8M;K;eE`o>^4WsOku?S0}`{&hjK#s)-dM*DJWY#Rh0WL@Ntqje{QZuJ!L zR!SXoify?#kl)4ueQ8BBjWP-v$&ugMP&0VsvPrTW?@Tt$-A)t1Zt2IInLilw#QeZ7 zZ0D#G_o&W|hfPGr-lnMA+T*jqOeeWg-|MF&Fpby9Q)9=M%#z0>G@jUdtRI%7!3A-W zT7~&xRzpZ(M#vP2W#iXT7xF*mns$$DlEqs&X z)`y)cP))OCoX%>8bNAMh6m6k(9d5|ltj{aj)a7oG`|TghsU)>%WnIjrT#z+3RA(Rg zUP@!va`+S@OFLOxGG#MGzQ|%p)Fjd-9F!6oh4 zx$%0_BmNW~&v3 z`7J7+z?2uqxwd?@Ua#6zdu?N8B_D_!Ht13v-t1_#@U#8`L39m6AK6A~I+>4q9(nRA zo~|D=TtV3DUu1sb+ z!$ld1%f=5Vr|_+a-M_?n|MYMDV|&xHklQldM#B~2%m-YUc}txIL?uim`T8X*MR%M? z3U5y}4X)cVF$(aK^@Nudgl5bdP8)D3zcU|XqY5;VhbK)53v*6EU)nF&O~<;|!0=vR z9yw!A{gFpo%S}VYu*!E@TAfVkUqR~JN1ZtekELfVT&v7-17ZSJ$rJ>Ii6$IOYvLt8 zXht6wM)ie~uU$m43E#2|wLaq?q7!op`EDq$LLEuX5%%FIs6>)P6iGeP(R|NU-GG?CFo9Av@ zpf^nqabon|q-MkD>dT|sWF4m;I9H|S^Dq)C+=_4+5|&9bd3438A76embWDfn4oTOs zXpwO1kdV!L2=232)9RQHQpFXuKLn#X$98-SNs3?f5N&w4qC--eps~&B6!JNHyRR6I z5Ub0JB1g4+BqGNeZ@D-=(|J`67k?isU#^F+GDcbGXlo~_sf>n;HOY`ep4>{u%n8KN zB?RM*&a>s-IES9tx9D9yJ;-=dRqux{!I525-U=6~rnM_jQnu!>SC6x@`nV%5JWFrW zBbf5ZfSpZaM-V%U7`ce^k@!GOQSuO7s=*etdbarRd8rCUSWl3A4boJHlkmfSx46I< zhfdj)idx4HK*0?~$$yj?{xf9`+<5tS8dCm2LxZ@)e~>E)9$Rb!$X^!&1rC4+vY-`twT2V4LExKIR6@rQx+Gl79>Y={zS`&~Y3F!d%MP4T!(IYm3+Jm!kx&B6uP?)->+tGlP{ zkhsw1v{P}aSCc0tdWw6GX4#nXi0SrD3#`Y$a%U-{0O;?xRJE4Z0u(q~_S(tlP zuvak;!5h=I-Y&~vW)`&Y%=4h`$P%VIbO`VQ=Le)V3#hge#5dDV0fs2TT-%{#5Z%S#|r zkssi*5}I}W9rx&rv|=07{BL!5>Ld@u1i(WVT7>RzT_Gyen=QN1k8}(xI>(@D2J^q} z0t59;2~l!x@~S>}MEy&{@!8B4r;6CLa*8`T7UXE0X^dLPBPFDp4hZw96&d5PUxZ_ zkkMxSpcJ~c%6^F~erd!Fj{D%|eO8T{jsM=wyk1v;)XQ}~(UR<{5l6q()VW4_;4_G< zVYOl5mji283wfvr{ECh7PH9Z?EhpU$;?hCjdM6H-x-?$Ao`ly%@c!ViS6h=Qx?nEs zl&<#j`Y7eD1Gdo-M!oxtnBz$US`$I@`$=NM?5F*bzeI+_ z?W;`Vz2B`s1ivxyr(FHU#BWRhX6tXd_P>EAM9!e^Y%#CvtiM@JP-1R$(9NVs++QnJ zkjHf27*C{_v}n=uRq;n0XU3P9Z||S|u%Lg_h#xv!o-{&Iz`Qo#)C;}>qG^^7k@+vo zFKr+37=#w!D=FXM%;;&Mn4_OPEh+mYSR=Z@-FgYFx0Km7Fwj8FS^oyS=5;!~Y&E}* zmpeGXDg7AvFRiE>ocJ^?>@4elPdjcUFgiXjIpePr#3Nx1kguSOYFq|N4l`67 z2#_l9u-2A2xi0vUgWH<^5^#O7%0KR{4%zEwB7FW3E^L{T5^yBqWe@bMid|2;;3cOU zUK=wD@g~9-?sf6lyC}^oOzU!m@!aKlMIs0NLO1Ct%dPB9>bjXbuj6XJ;-?_fz(M3w z2fG*4V8d&kqen$O_i)xzdPj#`(HXV}op1Hk-P6U_=PR7}q{l-U>(CtodG<0*oeLW0 z;c*TLwM)dHxKD{bVk$32mSh!81!q6!DPAXuhL1DLiqq~O3c~C`6(JYk z*>Ef4)`|EE8tCY6qMNVNLk1?Aqbj5Gh5C;igq7|pWEEJld${KQ82Y|%u4+F%0Q3=|g+=O|z@V4B)l=gB@b-}zIp#B<8k!~05iBO$|PcP-hU|j%5+albUm-a3LHaSB(1aN!9{uHK} zkcA8P`}Y3|HU`q((qFm90*-*T4315IUVfK*)*FWU@|6T21aWXB#iw1AlCtd?=$;{T zQ8DdO%i@bTOcXdGc88wWuG`&(q7hFPTqQeRZAD(90=e%{J3+p@$zMUJbN~e6=CCi0pR>6pe(@Epub`)~fXYh)c6T51 ze*w<$uf3LM#T?)C8i)&i>kNb`a6$Jqp;j|rL0(HhKrawY^{KGzk|s&z@+$J;W7GS{ zFA$!zvkQvTR%jr;smp)qgPDD7`mp|5*l{Nep&MA#s$IurVbkXr8}Y*v@x34*pPl$e zni1&QHgw?@3|B9(*LVtu?Vbm=r)|k zBXBu1c7semrhjKX8W?Wm{ZD#TQ>kQQv;EG> z{=C4{#3ASj_j%y>e=!*8zqf-r=t2h};(Kjrmr5X|U)np>ub^uU zDQ4m+4o6fj_JJAJZHe3e2CbUn*PF_ntUS9siiKSQlb^OBgoVFZ;=j)yFt0Zq{yWUS z$F|S&|0>;ROO^>j^ikoL^4EKhk)ucQ(lH!W1BAWfBPgXJJk69<=wk;pWFWw_tMl** z`5?3YB8L76Q1*J-_*Cax7096aJs^~&v#M#g4AyUS%SPp+v;ko%xUu0`Z_or|on@N9 zC0wb-j*dz}PR_G+mZE&r8ge7cTsQA3Zm|f;J1>>@;mMBkb~5 zBk*yU5lzfn2GxZq?w%~^UZjZmZbRp}=nh2JkJr4ff~}9QM8wEO91a{{fzao*I5LPY?oP^CTM=$b5W=kL)WG6YO(z2p^;(OJ=TEgaVt2UHoht%nL=6%^tjk-PfcaJ(pQqTK0E1q3Z^b-`> zH}%@rfdyyYlr1SA>|H?>?`>dSQl3A&JZ%J~coW}J_y6q!qhCR_rT5Tfsri{k0y0^O z3ZYoE9mt<-UfZ3f>c!CbYdsiQ4pMK6agudm{6Flyby$?^+BiCh0t$jkcT0B&5<@e9 zfOL0vw=jeV(jg$-A>Az@ND4?um(tzMjOTSN_gc8Wwe~sRwZGr*oa_AdA9GzZ@4Qdl z&;8ub-M6kzcSicZSg)$dc&Xz0RPeVeu`>L+#l>t#skK^+T$y+BP9$~t-L|53Qm7Im zSgLKr_85ox9&WgefFSjfR1V0pbC!NK4OrPYp&u~vp+jCnS35vq0^7twT@M8zV*UvZ zJ}F)^5ZzUF^mouW*4e@RrH`wkL~MR$r~bq;kKBw9qdzkA*L!9C(!8v|{ppAC5365k z8dS^IJp6`yIH_+- zh@n3aIheXKD=^ysq6>bI{mE{RU}ir{$fPx96=hAmMB_taKo4b}*O&7h=Y(Npgcj^( z_4$?Oj4vOi<(MH-Mq17_@!?ZD#T^k|bw#qZHe2aw4`&ZAhEus&y`z4C7T%)HgN(Q7 zQ_~nv06l#vZK%h(;dfm|?uuErX9o7#zqa6I(0);R%F_z8HUH5`BX6^SC15Y7nt#Fl z&fbKPzivpMEj@EUy>Wjx8F7~OiOsS+mgHH{ksV@sQy0fxiqfh`6|ejs%{}-9@-^<} zwlqfTYh~Xnr@{uArBE7QIcRtG6)_asy+?dd5@t(MpD88amlyB}x<|m@4L`^mRwkb} z0^D7PSHWlD+#w*hw}1f2=@AKAlFjlZ{_&9H$KRvo8OT2#O8)luG%dpafsI^RF_=xB z#(g4eb^W|YNet0PKH}<8Q`z5wi*f+vV=91TU-4P_AC#Z)X~7gxk(J|&4JCZUhb}f` zo5q=T#7xFjj~?($HY^_J>FT2D2)FahJ%VzV`W$I^nfRNpu}Wc?x5zMy`O+;P^6*u% zI&|~zH?*jK&%V)oY%sI0of{IOIL7lvt6;@F=PugU)#a;ix zF8WRFQ!~TWN-p>5C6&gop5d6n!llCS?EG)C)L1J<>I3zUt+lq|M(_;6xN01F;zS&h1i4eeD^j!kC6hb`dzYTd za6ow-rJTOk4J@{V;ZEOp?Qr+K@a$-*(@>U+9c0)ky?g7IvXRQgcmt1H;hKETSGWj% z*n!+$zFzpcq`2DLxVw+vqvxQNf1_Oz zpMEiRQJF8e5Dr!y&l$yg&4kFJV#h%kv?Ah$RBS#N5(VkYc#snzP&jdF3_Y6G>jjA9 z;46<^=ZW}l3)_P|of29Oh{Wf3f?rxS1~!#d`oL2F!5p%HQUh0qIs8*3+)L(y1-O~i z3JN9~T*XPZdw}#+)vYnVkzP|t;GW67P5|zRqA`Us`$|BabK_a|6+8H1hh@3-T2?nJ z+$%6z^3MmPH^#9%!LpiV4Eg(^vvt23E8LijM^_f zoeF<%p2z7c`Dfy$b9zl!W)Ui3kld+kG7yRY{A~%Ri!<6w_VOOe_u3|IrsCD_Uk2WG zD`y9x;)w2Y$3}j_=&NDi0xxaOz>a_dJ6smZ@=lO#r_0uBDEKp!p7smC5M96nfN~kB zZSptSA*Xqa@1RHv=$sxbsgYnL{llwT-d4=XqJFGE6w#EprPVVS1Ns`9?EM^uT2|~C z(oT6^u=L)P%2*MtzNbHjG7D#@S8A^0n@zSj;-aC>#Z76&cgSAHtnVat*Ozl#WkuXg zgsCuj?MptdMTy(G+YVyKOBn@N`uTq9JlfH8O;UdFcmj5ow~s<9FWp#Jkg0qJG2>P$ z5w)C)qV2q{Pv$*tH#6)5#zl(-4e+VvH?=*C63^YLoEa-ss7d4e16CJfYiwW(iXqol zfSxqau>)=h9D^U>)AXn0oN+e@w_@uL71BiHUqjjAb`XjowG8>LP3jGY2NPYxXn<(u@&U2@C>JmAwtOkZgsVBSg z1+UBZ57NJ1t@xTe;4c%0VDhM3U2Dw%wwI7J0L;UA;TRYETgbV>ul>$pxKG-6b+8aQ zG|f~>o?*?fp&k>u1G?vz0Uhd;0|9rqAD+5iwWPb{*BC)RG}>LFrvDa+UGQtCyJr~g z<4@0m!mr(4onJ(EuVh`=)JPT(dXDXU3rEP&(A+=^Mp0$0RTHpx9ml<9$ODi9@D>7M z(fcCL)nvbegq?m5aE~Kd_v-4{n#XtVw4-j4xT~Kx6@2@IXJ$NQo??G#&G~b!lEFX| zQCa@n#*TXHerYn?f9)rH!C!Tr!0rtF7D%q_mAOZ#w;z4(wg$V_d%A5m`{Ywg(8`8N z8P^Vib5jpvZpf1)+~*Z-{S6^ee5!8$az=ahXAvTFD=)!u4BhWtYsV?=j|H>*__Huk zqOW6S!n`6zyd89)|EdeC9STWGUT-CZWaY*yv9H**f%klUGhDGrXKd?&uAa?&@GxlZ zNNt_F^itFKb$@=@cbPQuUdmXjt-Ly%9Va$FPYGi_$*&`EWxKN#R_`X8kgR*;AnWq= zrDeaglKgdM77v!qm`HNHuwtZ$S3d9uG50gv>YiOoD&E!!Wn=7Ij&YPK?f<{)0AJh{ z`uD(x<%mY7m|#x<+U~!_m>mkDuqE2kp?_~AFGvt2Pai|ouh{r22&ct(@w*gs(ticR zKVjVndCwqw$o6;8O(gmS;ZLK{B^=(Unf==NuhXv%cH{`lY6Ab-Iq5Hu68H~rZl3r5 zQ-BjUcKwuWOAQg88VP?{V+P0WhkjQ7NzZjyf~rw%kmg2kJ&Y49Vk8A1#6bqV*Oy6& zvRuZ>`lAi#P1$pLb2;N9of#4jf~cISPLaopHq(gfo2$0*e@GhlEwM5`FQPOd592Tbkh+D#0MIPZ}Sv<7tJU?N1XVCBs^YV z%Q4I*Q8t)5x8*_6c)WnKGq3shvwZ90+Lz{d<>F{YZCsxPB!?{lf9|M<-#cf^HwOXUNu6fBse}^uI6o=7}39Y-&ZDf3v zofk;tzX>BA=U!m}X-nt9SI5xv!ihA-p}4n3Vg~FyRy_WV6%VGZsTh#D+`Lqaqa~Ra z*txvJ@ZHcBBVKxqeFqt)!@Yn@-zO5V&Pq<^0}R;#kPI>#uskZQ3HSA#fs#Fh@Tt*3 zmnsW&?OQakOXz+<%SZ6_StJ}r3YcpvCxx0L`6*&WDeISlZ~1(mxb2Q2^S zUg3{PV&nd@Y=vJYs^Ny=p72Niv{x@kDxnJilJx`t=tb z>3Xyr{ebzxNex0u`r_ix6@BESP7_J3zpVR4FWleeF8vjyJ1xG0NL3g1(qRZi(nT}V z7xJrod$D+fte{g;-%VNdcd<KFivzX=TvG;K1DLfIwQKrtEH7GDjDP2a0OCTa zbtEf(OIhmTSc&rM0(A@yMgevF`5A&80@dek zbY-+L!Qx`K+JYnxqQkqE*Q}AO3^H))Z}7U(Jc4_rU#&ntMBZ3+wm3;2alo|9f9L;d zXK#fv>X><(7E9(8G9&wSr@hgm4Rvh0j@Cm0=;h+V<)d*Ii;of;1WG9>#r@OqMeNUp z+uE@S2`ggceOkVQ`rds9S?NND0A~0F%ek`J!okTCm2V>} zQeU!IpW~`&bS3Mp4OA)-;Qyt|EE3;$& zK%H~F{p_D0{0jY0rMODdh37AYM=wdaB^$HJGfrcAix*?ws2ZJPKGF=m9oXUgznOJR z-RG!oS7kO(o|Y?!>fj{TY7#qSk1A4}hTp7wEkq!N4~9*sWzJ$O#7muQzvnO{ z=czbkAAin{bk8aN9z?J7slxWxN$ta33+SJ~Rrm**?6-c^sFcf^TMa7?DTH*`JM1Zh zwnZqqpe*5`$gJ_64!R2zoZ4>-dbn}5{^KOzUen%KesPBz&g%2dxFK==r2=H4bOT6& z19y9xI(i7?_URxvm1K}O@Lw!DmU`PV`3+xqyW_)hM~C)qaG(@%r(=UMqr8B!U$?IL z?}Kyx-QaaiA#^Vqq#}n?4CFr_=gTv68L1EJq?SM7Mi%kL$o)BZ!ry=`7JFMx(5S|j zI*1C*EalQ32<3YSddFX`m#A(84g?wp0W<%5n13aY_y)r7Rb&@<54~sJq{J?PBcJl< zt^{8DhtqJKoi~1=B4)_<~g~h{R#6wsLwya zytX-*OkX`V3X2k_=d{bBd|qM;&$KfvBZaPw$_#(~M{}aXU8}#_@d|Bmj)#PggoZ@e z1tiP9%6ipjCG@{}3&Zcif?-GlsD_%RGa&yr=Y!irmxG`yWP8W2q5LW4x;LHw0k(Y7 zJsvQ->dk&{;v7Bgv0|Lo*@nJC?DR>yJdLk|{Pwzu`F@PdyE6Ob272B& zot5!BsM7a(uWDyLunjHKRq9Cu+TTSB46w0A^E~+YbGKXn236wi$Q0XJxYuYbx`{p3 zEe_;jaZu#c3g{(|ev*XYzZvx7vY_3YZKD^9$b@d*0_OE8od;Qk@h_IVc1Y!d(thGM z*WYdQsC{8yC6X!+{E?7gVi#Sgx9u{JFW+c|vP^F5Uz0KNG9&#Kg5&NEgKC&g-+Tjm?U98NG z{x!fV@|t1GRyg*3>gi0$elrn47fRpWhlq!z_6;I*bu9_zla>Hm6#yWp-CrXpecX?0 z8n-&Zaqn;TQaVK74IlD6vQ=L=@-Z{CwedxRlS=$FR^>v zh#A9}O*ibAK~qy4BXf|XI!>jOy0??j=S{jrGTgmxLpPx&$SHQJzrr@;-DzU$TBy$( z?>xJUZwt93c_c;S*A-riW6w#eN+!XVF+M-rqYryHtuuPiJ`>YZL^V)pF_{PyeT}M~FLOykA@I z!27MEcIWE&m+)8f3V%*na@rJN)|oDqPRn;$%$Pqm(&zttqVTg9FLgJJfB8q<*M*Dw zz28B)JAiG5$r$$giY?9+v^T4y$l6(>kTYg$QZ=I;DmnA-(&+@AU3&wI!-D{d6GI5q-v8#q$&7VXs)~P^Y7ccae zYQ5iEP*y(Nxp&QAYz(ccO+B52wt$}s{?28ag8 zUD}8GLaQIiD-GT~`v-)6pI>Nqy~fMh7O!+}HMX?qchgVZx*w-96*~sU#l5H>bj9xB zH%z{{8^F>HYbxuKIBJZA-Xh{5f|s7ZZ_a(Sw8~MrtIr*%!n=`jIri&Xer!dE0KY`8 zawkgHxO(|4QCP~+=ycvgsZ~izNZ^|Xlh1CU3!HCA{W8wdEMvDoy;tCyv`~KmZ*b!E z=0n5D=cC45nMUMK^#g|Zk4fTaep!B`eH9)P6`HYzvUKMz9}n45#t<>Gr&t9&HvepQ zoGGCP6~Y3)Gxo2If;9r)SBY)v3Vp>id{N(acCs!&8$U7RwA#1IF~Y?sDR;j76GMK- zM)4o>l0l$gp^gVLF5hY(W(zY`HXUxCkMW}$T&0V0ikMo;$p0^U@845wgXM^TS&G6C zJ4!@!g+7+`<>Y*rv^tGC5}Mc>!Q`+XUe72?mIB7VII^CBOmaBK>6m0KN~4J4g7qm?H6!NfU!p1z;B)ydc+m-2*eK8JZ0=UI6d4>CVGSQ9$A?`t=R zjV!;Lz9s)V=92z~`&kFOIL2&Kkb{o3#@%pbP*IrUU0|CtIS&Z16zszEF+sifdqdz{ z*lbAj(%ziV)&<#*H?8q;^Juwf#fYW z>7x_#*S>zB?D0(+$eUpQf9M>kzk{y5z@I0)bUj?`9}5%daqr&U3ORndTsrDW%oz(= zJNcs`K|VOM*?R zs{-AgE@JwEdTA!nWkr|Uf!xAMK7g$7QTfaa_H<5asXw}6FAo3u_4NR^)mxi`wE-fn zMmv3ui}82bgcZ24$k!LL7l1+(Tk_AZ@~;l3zxIhbU_~HFXZt&BeUz0gx+{H!-3-*G z?6VsHqEq<)_*NwjI9<#G1r1l8tOC(QB!7I<$O`@6@<)%M|L`oO$U(z85xUPyV;}q> ztn;0vx-rAU3Bt@+aFP;p1kPjR+p;7WdD@7VkH7?#-$92D1K{<&*CWvqPfnHH*ku_= zG%di#6m-z?uO}s_dJQx%*VrF{REff7_fUw;azTJP+@NDi@{~xhrgWHReG6a>fX&#f zJ|Z;LV|^!rZqF(yKU4xzfOiCqM{e_YTd6=y&~hGl+ik zs-xStvjj1r>bo21Z0&b2VL@iLUsOG9&f>&yg-)so2If%Y=GJQi!~;6YFfQ*XcMJE@ zP$|bJJfCl?nrS&t=COYGdFefa%dH?_KGjfQVw%;@k<|Y4<3BL?XFT8A|24 z3|eH!DF!0n&JXH=#4)nh;TF%4!G92;a6LFUeH|Q!Jv1B55BPwPjC=<*wq9{r0|`ko0xJ)l7pGR;+EtUrQRS$qQi$`#2-&@ybL* zr$f2s{Ra866hJTK;$-A141>JgrC9ggh#E5sex|HD0!&_cU*pa55ewlUm7#ZGV;dNR!dyKgxPlr@sT zxLl$+l8)z8tGSASP5@+)`G=ix4DrCY4?C zkQW{Wq{JL@Ov*0mrOXuJHtAN3$;oG7iFlOwL?@JoIh}KH9TN2sT24e>ElTyb0)13z zEbvLX${?r`Z7yVf+NDvWmTC{*CkMx1Fnl7hLMxT;_iJAyX{i}k{&>U$$yFH8Ufp({ z8Lu5>%BwZMlZ;P1HMYzDRpRbe+Id07ur1k0Nz^oQVmgfMQAA%xEq72KdTWXGM&m$j zt`q-*yNIH5z6h@%J!?QE#M2eV5|jEUHb8TZI$rB&LR%8y*)5j-X;kowWpUI+mON{b zfz$(`O2NU>DsH?`c3k`6?HpwyZ}DxB)}-jyciU$C z9aPOhSs}FlU-;Yt*q%QLm#PiGWx3BnCmzEkstXYRN_Dfg3%Q}XskQqK8e@UEh5~oG zX4gLS+Dit$2~_y_QU!%)Lbs~*E(ylpz_GY0?yp_HWNO`dlRiMNuFT(UKiGzHnR0Zg ztY!p%{F8RYakXAngY3V9 z=t0>YKT7eH4nxY$+_D6a zhQW#F5>SE&Ee4Gob{QMA2#JvaJQ|Y3qO}I1THfTiS|F|VvjC-7lc-8Hp+N;1nEQ?< z2|B%VZwQ;qmCV=I%MrXK9yUyQ;gs?7bPokLD{tYh^C^9MMT=F;mZKljN#Pf}B^vjc zcCvkhdFS|QPLXgb=5Eqr4_Zl`BgtEu63L>?2ea!Ay{5W^yv$-w?0K_~<-Q3-o?MQ@ zKIYbpFH{2+$`+bd$*g@`XyQoYRy?d$Y-Xy6j7CY`l!jFCdWqN8jg^s&7%v!GPc5s& zdNQ20pL|prEupLp5^fHzPqV1yv*Rz7$2%_VB9!rRpXSjI;k0Wm@I^ei6{V>a7%tu{ zY{(mS8&7mofbJ;fn}cKb{WqG!)q*-BG8Oh~j-8Mp{EaPPR=dOD}n6q3ZXw3B8grVm|N^JEo7 zUANH4Pe|Sr+1wf&EoOWDR=?+gE#=mDa_TNMq53M<7xS7#n^-c-=&nSPOkeQ%-cc3= zaJ2AAG8L4sMJ;FR;x@Yo*(5<~I^gxDG?@Xn$0@ia@ikff7&xzyrKU$yb#g8)ru&8RZ#w3b%wI@7n{ zC0hm1QYp`ll5(A6Ou2$MHsxRDYW#ni)A4`kJ+f9n7fl#>xxCOb9%l+LX5Hc4Fh{#( zD`{U|*iZfq3$OwnTmw!=*9E0M%dh{AV=J~p{v#?(>j@Csh5M5)_$@#Tdv8+G+j8K; zn`oyQUZu!VgzIpA)*cgkJhyi&OLg&}I-xhS+&hEYS2k;aC&bi_2Bj<|T+34LYz+KW z6XD%jFYFv}*@=a7dK^N1z*oyawRC`gFeAO#c?VqpLLUQ3r@G-@zrQa;koF>dArgK8 zUXo0wIa>leKjhEH^-djtEtyV$onV|Rz*3-VwYfmpmM9$S|1Z6VQ}RsjCVK_q4Q*+p z{|ODr8|zlO^<_4s3vVLcBD;-PjtKMe7W5ua{O2E{>pP3__-Ehs-!WUmUx7NLQ1Ft1TZ?9s)_hMlhyrs!Dkvqwg#L*-$D?^Pm5Q z8?PZa9JAYLyNobtRF{iO@WM%E?JiY>UV>F7zP4GOFfm?)lZ4+pMPd_^bT+D3Ns5AS zObfo27e^^jj8b~@8uz?J@(BDS(f(3gw(A)=36FNcgWMJcKMBo(mE9~YG@kXM@%IetM!ESup$rmBO$j^9u*#1 z?sqpgbD&JvKhigt5~h~q9YU_<*;1O<^v<&~C3pRBHRzefLz;%MNRK;W5S{)aruvz7 z@M}WlaOg9}M6V4`t*iP(Uxo;JzbeOUNlb8z_j)Tugm83EIdzQo_>%ocef zSF@(-ia!Q2+RqtLn42^6*^C%;QlKTR9ZNjWY3B4mxvURw#kYqabBI4;%$y)r-Wy9* zWc$SdD;kQ4E{144nNN-MnuZ3Wt_;tL+o8;^PLAZ5ag4nOa-OG3uQjE+eh3gAIw%|- zd%F~g&;Kcb@A@#_6wbG=qVF_V)!1)Itx)sS% z$_cTdJ<4+lWYohC^{k2IRR-?yIvzwc{#Dw zEzX4{6g|VGbGZq40vwZW*+OHcMLy%h&!QwLKSkp*W;&2+W5x(zAPUeeOqUl$Q$V6` zwLNRIG0a#-cbYuik!v1}*-1&v4gDm~ER2umFvt<*E&ohOE)JE?&Vz(PJy_~b5L@Vv zFGnqS#GowKCE*w|^A9xpMT&S^Hf|{hE-+88oaxgccuUbnx2LC1_3?cOw66I2{2Q8-ck`}-f7=7@*rxh* zl(j9X#}&y^)FIO*c4Py%Z*kF)1PE&-wY`-0H;Qc4dK%I=KfDsI4r)WBVTf)cW|Mnr z&2&#pCJusznEsUq=8k1f@Y=(J5hwan?XFHLR+sd$i7)&VtVuqms4)(Htc?B z_A}R`C8bx>r+ga1_sJ3Dj`!M@Tz6Z>HcoE8+Oc`5C`JB!r^>D`XNiaEk)ZFly5>2l0+WN;s2J@*N`6aW#BQlT2$X`O4=Q=W%jY%`Hs_#RxzbrY=j~_`8 zP}1X0J}Kz3chh$vb`T@ze8N_w^^s~&J(3CkE?$?Mn@u-mt7il;o_7*eLw&pyrMR+P z!ow)x3m+iVbwYq%W#mJJOJlu8C=UB|a)i)rBU`q^h`D#`OkzPAHA!dNA&^*V$xrF- zE_lfH>mHOu-V7pz9me`mmD&ReKY%61I-7@*t|gCVf<#V}5--^mxN3d9ja9V8e!y6~Erg$u9LX9!DS77|Ql7>vbA z>KUFj>iW{BIB-L7a(}5tIVY>#8B*pLXgI{r0TBtrW`egjyz7kl495l_yQ%W|e3KZ(;m?)~`WibEcL30)BJ#fMcG0UHF+gsvan zxqogp{1c!PY)L=+oc*77{{x4w^8dky--+OV;P4+f{M&RgSBJhm0y`>pk_#mHHw0J} zTww!kQs~W97ms9%<`{J{;zS@l{TO+D2Iy~qoQ^lIPnJo}p^F_{*eOscxZEQBXqx3} z`$rYx|F>UHCrQljn=x{B3j3-~0Lm`d!r8H+vS*84R%~_l74TfDwaPB`nxmLz;9XX^ zi07u<$F30it1u+6qu5`o?wt{@Wf*nxn(=Ge5yWdI#Vs^+;?Jie- z7`m`A*k(D(Zwcn}!{Z8(Bv-qEixBsyV9G}07VFCbo3;mRDQjTuBTY^P`-(SA#n?F} zuO>M&UZaTj2TTZ7kDBDY5#t^Y#H3d4s@Fd0&b1mApHv?rH6wIbxt{~LD$%@y$*(;- zw7!lH>JR8MikB^_PIu6B209D5^hw$wS-0sEE5;EhDmV6*W19#`%&hcyIY8Vb%@#zT zTfZ@2sQ`4>%iZX4#Bt}{k(}`b?%Epv<&ntr#`{v z4T)c`9|5;{TIj`>Y1(@A+@CATRa3nxMi;oRT~Mlwr%`c4Mnx*kj6|OSmn!lKl`p!CefH={N!Va~NN0k=SSBQ1o#ORMy?XSN`GVEeH5~J-i^$!G&goy8B^=L~y;?Zw^^FRpj#gfnYql(H;IqDYsD?EJ^ zf#9NexgqP^2ne;KXUZdRu5ytkU=YIiQ3_OqD^+Asa2D}Vi=Y|8+p!wrx@)hG@I|+t zJi8xbV}fT0zl&o&lnsy?in^g@jBqhGd;TfPehmcch-TAx!WoSf^UQ` zJKW3H^LqIrgB8so#RW*R8BR|hzMBU)rD%qFbuIJ7`G^R;wO+oJLDUVvOFST;6YV?u)#8G!A6R^#rV`|BMj-_HRfZo`Emf zDMfq;DB{*LBN`rRN3X?iRhfKa(mvM#PNZe$s$ryn;jd-k@?r4|^!zLo)_z?i?G?=~ zO;`D-qJmh6i?uck-Hp_H?bGZO-^;@T)VAJE{)IEzKjn`r`GuNRG?V_dXGktM@V>RE zEBKoMU}*gz$SY|9R3sMm1zvN*qG|;_8G$yoj#r~+dO3~{Gtk?a>Sxn{ypsC9=sgda zofUPQw+stZA#|7pK})SPFEIsjD)JX7>EQdzJ{fAv)5)_4M6`zuqz*sI_-(0{t06g0 zyG$==bU)o!Hwel`N4ehQOBL-&{idb$y15#C2Tu`;=#&cc^D$D@HJ1g{asp|SD+C{6 zB66WWH$>#fcA>J7`Cewjq*nc&3_;!o6H>uZk*(&yCf}B zBwu_g9*?p>W*+#mS2sOAddsS2eb^+O{*35&P~3*L4K(6Pfb*MQ0AQCd*o7|5>z!d7 zzN6<$KR-)=bzB$R3SWfH{#S2~qQ9~`KkxMKfQbSw9?)a%LT*6z+rzyIxKP=`IJpm8 z2!O%m^K=2=Fx1_ww~Mi&sWh7ySF)|UDDIi$9qgeDA!;4!8PZ9$RLXm1>~TN*)dkXo z9qUR^CLeFnrTn#jaqE^pbe-bOtTY!@9c`MD*dU<;BZQtU%wb|-KX-_1MS}kl@mX~K zWbu+%lvFJ*D_*3+$|1e!2=N!}>K%c2!3VfKdilK{Ep-RS4=6KkR~MvQa)6+zYV~8` z?~kS`BySm6=A=Ib4V;C=X_fG(YdBsLW|VW)68C;E%$17f>R(aqf{-i}thv zcPRJ58Lag@gdgx6xL5+TR8RmQ!TH=-hYMOC(G-K94G>C;?7UzwMt2%>>9#aJONg#n z?$hT!jeHfBIp2rH##d*=pEg&p8s(Vz;+HJe(_8BH3f6Z>Q<`{+p@~ckmF8HAjmQNBkk2quje2Bv9VBbM@aEe6oMt_Ld zfe4P;0T@RrN!7JnG^s;k^^WxIL>U#(|j!M z;kHew$nvXr)9^P}Hi{1Cd2OP^44*CSi!ebBmpoqZ>4#BCZr`C`E}OG+JsH$HMF{A)8}gY$?S< zJVT~`r7WANL+Cns)^Uo{aMb)Mox|46W+u!`;AphQnN#7@o&#->`WnSPtgjanp8~;- zIwg*VPhH9N!Cpn)O6>zUyMWV;yvEC z7YYP36tkRPbLTs5+ZGtJA-}LPEEgd*Z4?740NvN z6Ry=U@3hAW-nCU;Ryqew98pPk5Eo9l7FDB^ftCZotGK0m7ri{7U6RBN#aZ(wC5#`q z>&#ahLL9fZ@oW%Rx<=D&t+FN6@FmdLC4`441LU$3gXn9*N+kMrWmWr5#{dUEGfBl5 zh!UUgF4h;6Yp{#ufwtJbEbpU>UUK^6oK3ZW(fs;jAPtDxNJniQ?+h{e-ZN)+g841( z56GAmIypoKzK*;b-O|@L@%5!XHF!0VdzsToFATtU3mc1w-l*Aq%9 zm$y9GpfI3u?zwuq3T+?rPV;c>-K#-4l%@GZ(pS5b5B3gAaksPX8a{a>#v>3CA>6wd zvPqi_XcqM*-a3|TT^{l_yb!P-a8$??k9VgMejo8g*#B<#isn<<@Q>jGyWc@{8TF}o zYUplgOaArUOG%?y5oR!`u4pB^wTv|@gkAzmN##rE9ewE(R^-gBfQ!+B|7qW+TReha z0mWzQF{K7Tr+z26s&D?1MPM`etsMPsQj-dvZ@w$r0%zN>pC26}ypi-E%b^LS300su zEcT6F>f4>Q0y4zwR_U>X)&D&ykQbPL8kNU5&!MNUzSJUEv#mP!kEe1}|w` zJ((@Fud8$4eV7yJ1#0W>7GU-EE<90~Pd@@&2^TuxoH$`&_)?@Np8>Ny*z_uh9f##a z3cj8{LI+r2QHUI5bWn{R3+i1UfMK5LFr>0CccII-e>g)!j|p0}ySo2pol1eL9upP% zo1~Vs0CP^?EjuZ=VKMlV(a3P2j#myu?b{6DtN3UtUnFcmvQizTwH6bC8@pgfT1r1& zm{`Sb^cE7kPYe~YqKEG?geqbBDwm?1lu(o+UE5YtG(PBSdm!l`SB!CQ6O~=oSkFnw zjfR&G!>;W+h)r(de&O6jY_dg;`m276bJOJ`{O#^Zl7MK%B@1Q7Q>A zdwJMx#a?6M_cCSG&vLOc!d-XBmISIM-F7s9l)b0WqYY%(6)BKF!#n-@QW(et(|xXV zYPiO!JD@vYlP0BQt_{&*iS{QHY``y!*Jrf@R`caI!b)L8GfTQ|DZNq+`tMRr(mt*AE#gH2j9F z&ji+EpJ_afOZo<-H@!~-UH6n4{4Pu^dPi<^mjtJ+LROnG`e*43)OAlE@e_NRT*HBmJ0nR&fZ33c5 zGzUb1X`p^BCZ7oAj*XM?imO~AI)}pWNeesF6=n~D(YM}MmQr?A?dy(iyGK0heZCK6 z#TV=yAKw(KRZ9RR!ofCp0mv>g@~&JcX67F{f(+Y?x^Xip(-2;&SL6guzS=A2_*-f_ ztxNMPXUK=|7$`+kLX{iX;7A@(p+_|;&CUlcCY!HnJuXdB+=))Yb)Uy``PcK^O({qFgT_1Sa|Y0rCbGT z-F=X?N%HC-$3#Z)O#g*RZK~?IJUK^BZ5z$*XWI^Y_NOdL^NmTyY*ciWT+>aVM!d@@ z>0_UhM9n+kqw#z}O!gyBtFvU3RX#y7b_}{aJ{-!SZ~8AN*Yjm3JP#9n|3ruCHBCH< z?GtO2%?w8jz5cwc(9(7z8o47aF()fJ54S!eN8~}bT8=K>OSi(2P4-4f&V+Ci8^N>i zK3Yo99qn?;^u&g>k-AjTYzAk8fPfIl5+qxF>3M-Kz2D?LHB>yQqEBQ~{6+VdWYEdU zknXiV(Rb;RaZ8#!AD3*>)!A1J5o>CU z4Dv8f47iWUSJTn2EFn`8k!XcjC!Fnz44kHO#zmQYQ6|$6vF*{@jgi0YkYW_FzMi#J zuHIH%-`waNdL_-?L}kZ57o+G<1xocbi@g?LJ9+2atEnSylC)eLJ1zE&krv&@dMWH+ z+Y82E8N!UiyJ363WM?}0)K7QWBhcSx=N{S<%CIP08p?wr_FkriSEa;rp?Epdn@Bps zJk~};z8ECUsvoh+GPIP^YGag-N}2YO%;6IR&LWksQ0`J;_)KmnnXA>QDWzBIXtgM@ z-0DN^nj*59ycdzNoHjc?hqcRqhl}eLJ>{`rd5@~Wy}xBl{S61gk&U$($k<;n)e>o} z^cIhe;zRZ}60E@J&Ygxfq5`HAT0Wt*kw#0y&QOj%+_Q*G^|b>0?jz2}K}L6K7@`^9 zHVF%wVAyVQw-usK@DNsfIEq2vrEi+;ce8R#H}?pxO+NFLxab4a+fcPDoqVcq`-;Uy%rrzCDXE4tVIhCRif{l1bP z#p^o4;Gx_4_DBw&%_8FX6~&1lX^zB&NN)E)#u=!ux?M=eV_vyUcb>KVbGVrHsx%!Q? zw!gT(|CZsFyovOHwGMby5z6_l1Q=9R=i-G(})- zb9J@;U%0h8qgk?Ov6;-)6S_`&aKSCX7miM|Zr2j`F$X-)DnD9|e>Du(vyQG{ma=8X zGupi*cvXCYpZP3_Dvc&Yp@9h7AjOy<6TJp4>+yp=N^ig1w#l%z2l**y#7MmF494Qk zg1ysBB1}6Kx#!B-c@cRL5$mxuLRO|3*lOy<0#)a~o|Y@BsDa|?`OX+Fk3u5s9!?2i z4$OwN#j6@CzNI4apFPBw87F&Nbl1oNhfxBOi47wxFc?Vl!v?9Q$s#!0VPtf z^30bu7LHX_AaZgyB7fnl$?iU{Q$UGoq3i}e*kic@lCIb=<80oEQ;B*cth$*)^uS@4 zZsO-(iue-yKD+-_8UNP~9JGeQ2Jp<*t?JfGLKVWFI>*A3pvzalmIF~;>o&@O1pfQ= zT0*z`te1gceo0PdBQJ0}A~8(fwuw_U&(yVqG-cdk`JY=H{`Dlt2N13nQ_N*X8V(`7mEMHb=HQv*VF~3^ojM;QX{WwE4 zo^F$!G>h;8T@&1$=biPBc^^CAy|pDHZFFkWeY>^G)_g!&$$|EEPzil#IJ@GvAJ1_+27Xj{8ILe;?La z+lwg{+@34u_(=jmpB;&JYe$0sry$2Dkmj=r)fk(B1z+cgjD1lpk8eL4K2L8O-jI?I zZb9Kg%h$Z({__^b+D82Qy;pqm>){Bd$pcj7aTXs-Uju;zXnc3LO~(xrbEAdg$D;k z6FXHHt`+-ieVSd89Byf<;sIq3=_gfF#A`xk*Y{}+320aj(!?u~CcL`u44 zgCO0V(jwg@4Fb|7B5XoXkZus^?oJU#pBT+O4WCkkB6&=4LMk6QBNMN~Fe#DYewc{u6r5mP&hp`1;;*Pav4B zP2Vy=(HYFxu5?m523Sc3)5;dd@7BKLtYGT}acSgNzN zEIHKau+w8GiYE?3n{y%kN7+zVZou6teX6LrtCKe8Pk+pMBYOarFKEqCW(cv%KR5I2jWD3g1*dfpNz}%bgd%+8 zkTR>hR<>JnEoj9=_oz=$Ldrg8T1Rf>VIKjgNBohR=(;S77muKvtaXFP>kSsbZF^Z$ zjGh37m@o>Io%O6~-M%R+n2Ii~aHXq2ip9J9L>J3Ddf8wpaV(gl1pQ88n1Wbneid0R(&xWwK26}#Pd&1e~RnukaEhb{3`7P!Rs z?oG`&@gSXI0cK+Fr<6*VGizPp%Y@*F>GEXTxKV{|o{MsV`dZ|{*KxNKQ~D5H)`F6^ z5*kTI*iLoGT@(F=eY=7jcScl4^ipW$GMYL1_GRwNpqRY@TTpY|85R`WFc?}2F?qL^ zU=i(&aaVWYKFAJ9eoWId!GGf3(Uo)Hq*zBz&V8KpfqezGaC;Xs_!5^ zo!_kzc@J-BAH@E(DhFy+|6Qr~R<41pUh>Lvt!_(p?Swm>>z1?1Rn0i02d+3QZDaCo|mD6*8WV5bZc`I>BYUp{bW3 zA=7^wZvtUiTjQN>B6*I^esnK4E3yoE@!Yy|Uz(+KGVTlV(ye|ZlGTbQ*><#2nN-wh z_;*^Gj2_f0yzLa`Qaz!`;u{7J8j7=49CVUL4A7M-VxjX8zzsM!YO5-_(Bq^HPZ7Rv zX{beb&4v+)jma9h|FsD&$SLhaPPWa3y%#^8JeDG;h}5mk0V8)Kq|yZt=-B(h;rQ_* zG+xJ2;(?7~;8{t`YxiaboT{h^i+6)wqrSSFb4fJy+~r4qem>@hqsyRFJ(N$ar#Au6!ADXWPh8Lo|>XmQ2#(LT|gR(Ufq93JMSenjT`+jP9 z-m@=sG6gMTJ}CF1!c+r16@NIrD{ZJhN!xaz{gfqz`>}iN(PmIzc&T1q@2EebgM<50 zq_>PvOytA8wVu4)G7f{Hq3$5E_-&p(x=WxxrrPVN2G#f)*u7MA-j3Cl20kJmLr|2) zX<*-72B7yA3T$H{ys}qW_G(i-ebmi zg%m-r%FnuR(Hn)H2p;~Z2}seB_SqmU@ID%CS|I3O@f(6s1DUtIL>{yLsTKT%7?-sC zNRDdJLl*i$3K|%aHfm!~KFUy3oFQFkEQ2F^_oLL^X&;#d6B|OInmsuy*_Sm5Iy#lF zABpkFpnt6av9WwAA&(|g&je6Cj{!>)^8AHEiCr>#8Bs)KkesW&tX`P~ez=ZAY1U~H zPkOCUJ3pLc+XHFdDGgpEfz`ryVyc1E^|G-wZe}$BjPENa_uc&Ly=#)g(BGe#RR|Qy zLLocfL0_;MzpecF9U(GPGntP0aVO7vzD}Aej4K~=g~N|46D;XG_3ty$>_Q7mN01?0 z%W3@%Leo=lbQY*)Yv=HvySpkWqXDx5TC-dOlX$^w^0aF2SCXFsiN1gY6_UX2i*nNr z1p@d0xpn~z`P#*6e$g1lOKI&~Zh`q7lpQC0XsKE$J~`7*rEk?BLm50K7v5Y_suCWd z`8AARZF4Hfq}a)v>+Qgc#(h}G*^%(sZT&oKAgmt+Fqs2r;p?29DFgp)cKR>G&t z$SBUlBk0QxNJ~MUoPD#F>xQ73wd@9GlPlM((y=A!Vd59|dG{B3N@K|7W)(iSS|BaM zli_k(9lwm3(%gwV4X^OErO9btLn>a=u;Ey%YkZdxqgoX~C8X6B_!zmLR`<;mIaL5mF(?`QSeQ2C-ST92>D;#4TVb_ zR*|j6!qpLGC@{1U0)qTxtrsH0930>rCeA0001MoU9^K5_l(U74aRi?I#9i7vJeN}o z_WKct(lnvk)sBF!e*ZhD1`;i!1Rz|uzi-?nUy-SgQhHBb|DVQ$cyv?!pNTw?20mFL z*7Oi=yW#!C5Z2Bl&@uRR*ugh2r>kkS3rDTLbQ*|H*FR}W-*%Zl*u%g4LU)-RF5t&U zw>GiUxHiXpzMyo>Z+m+32Ko)hU4dfx{-B0XJ8***`U;tc4gLA;DL16M@`1HKt9pb? zLp=abvkwSl!yUY+PQQ2&?QxQ5he}$}byad>;5M23tZ~mPp>H!X%^2kHoPfDdf zg+*#9pwqvZL^MJqp~JPxJ!*zQe*K`8g9|?yi7xuvobIV4Hc&B-th-b?qAgmc`f+Q? z@A-^Vvn`4Ib^^oD94qdx%9Y*D!%5<0vmZE|J~KC~JwVnt+;u zF?ZWsjmajCIJu|y{D!n`D=l<$57fCP;ONQT)`D$|DUQ>ln4S)*G8C;Pex$OpPiEDV z6k}DNjwVZLRMOa#RIRIJ;V3w0wuNVXp?BL+H(|s~%yJqSi!vlITgZ(Z$<Zp;eeT$b-)3Dto5%ExoN*B9$U^Gc$q-16c^V zTD6|(E(L`+CN0~mJ6Yv&YLU%Z9k`V1#MHi~!zk?z!S#GX^c(p0)22q`#vSv!;QP)f98^Scm>? zW`r?;02I?j+%W93%a zz{ERR6*oJ=A{57*I40Ov%0_ppJ!oBOQ$dMCK6U$j@cYoWUg@krtak{jj;lD3&_KdK zr1PA*o%{=JPKi#c>vfyKvHAy7#g=y=iIGoKL2nF zBk==kl89x)=lrkt(j6ULbMf!>>SM=6vC(&M`@F~GIrVOyO+@n|r{y%x`qOme(A1st zwwxT>ljM9Vn$!F&#W>>|s;j|IR{6{{y3iTM-J#f_Ow&(cCeL5;(>cph*to26`0$ho z-YpQ*BtUXf{sQ#MZ32dX1g3MxnsZ0gP;14CD?}4v0EHM1$8h|akxpb~-B+J%Uxmku zr9k`hzqJo&`mfrz{jGiA(zN6Zcz>giUX;o$w>~YG7ItELgG?x5*II6K>=V?>Fy^oxGNW09x*Sx7om+vrTtOxyv#nQ)Z z`xZqs)wSM+37CcZG7VZfJQNGAD$|vE1=H;B9L}K)DS*m8vyiT)J_Rtl=llaO%{Pokn;^lTjEyRT+ zr*D4^V27>%6OFxif;l6z|GLKwdJi1h&09!ByJpxGSlv;&;ff5TBxQk{w${!&i@oqI zYTN=`*}&>_sQ+Bp3C!8hZ*e++K>~mfQyl|lBI7&A8B!?bcI~YHhYLs{TrOVG+k%tu z=wRQHZK)TRF#Vsk>Y~DQCo3ND82W}ZYR`t_iJRC*8;Fvnl^A|mE&|oR=a>4@!lJMU z`B>qrCzAY0W6q;|s@_c2*vmula`;jh6|h9#u~d<=slF%ShxRmbJA{k6)^vuAhcxtI zqhaRc#wk|`9XlF(k|vZUp#n=R78MQ?=Bb0o#zn8JRl+(Sp{&RAj%3cVij&)DCh3^< zvJP&`?;3dW_BY3+OMA<>&7?%~kcnIJ-qso*^L8MS#jjRnRQYH=ViO~7X{RQHQFcs9 zer$=A{gImClar@@R7tcxBYuzA6&HzLzR`rw$Ud_%N(1F|5H}5%|w$4@r&$4hxyLyUA%f&vC0G>s@14m}btFNG;5U z3$m$t&28^ez&@rHCAW{TISpp>q z9MDf&1)v}Azi{t{euk>T{FhA=fyLKmia^Bw$TWC;vtk2)hO)FWs{sFZV!d>PSW*^? z<3IHFQ82KR+S%`_rf?5{RK$xc0{tfq&yy<2kM#ibE&pkhcj=w_v4ZxkI%#jbu_~+C zPgjxOZyWbaa6fm>YBsmB0r^Z3cv&}5A~d(Lx4*Dg85F@Pz@kUg3}ah^r|szMyy%sY zz=6@~37_SE{aC_8DVlx+?+84i^8QqKZR`gwW>TO!EQXN6>CxK*1t7W7&(amYKuV^$ zIP3ilYxBDJ0N|fMZsu{0E??(v9-f*-de<4D4O4Mvkl8 zNH@wqxCH>c(qAy20Qsu=vvkFoP-6fYa4_z3dAw>l7x(>@0Ge+NGPwdb`p*DQ_9FRU z>o+jZpQS6_^gnrA$k#)-E?sfIzZeW0*=pahY~}k(%^?Y<54yda(d1Jy?AB4C+`@C0^MVTc+UvCGkv&X3gbdCo#u2;>9PKiy;QU7|Y$0Bu%;BAj zY1Wi0F|#@{L#WdvO}7#ypnJO?9{XlGIVp9n62(DGMlSl-Dz>+!nU$-nr8A^F1#F_-SAlcKWXPc`B1F*FE(nnrY7{|FG1!t-(U zl>tMw^Z2DbWZAM6^#6&K`TzitoWH`5ykVqPs(ZOFidv!LpnKnp5ETQ`5BIg;o%Wo{ zQ&ay{F$Y97--Sz_jbzKI_pf&F%36@oy7wa|j)jxxV`Xe@6&V{Uij$VDmA)m>Yq}yj zY0IrQmdPk!!zzoee=7b4<*3GYyp>!&^)aFglRutnA`gYa0fv`HuwIQ6PPTq{&W*_| zD8TC;6}*?!i)35NdV34j+H;A54IDpMo(RFsCTuB?4|3<;Te~8MG9zL`; zWtXOUZG*8dff1ALGUrP}?s}9pgqK+gd8%O-3tnE&eh0Xuk=5p8k6K&XEc>iJF`y@^ zi>JO-PxS@n%(D_r+c0FeW8b*Si=NAl^wq8QDorS-laA4*MKz=_ScPCC7|=f~jl8S7 zty^rHKmD-8-zef?U<-m#o~8-xM3Yc^@shZ9Z%|KQBrB5$VSv|KY9}d%@xbV_Lycf; zEQi}4j9pd8d$d3x-z20ip60WWfDG$k%-(mosF!^owEBx7LvL5`GV|Tf-Q!Wkd!&i| zm7-XVlqCt=gN|?+e`i~2VzNW{@-OnHqHk4dG5SbLoj<}54$D9J!{F|+&hdYz7{S#1 z8UALJe;o>EiV_sW}y0KHizHLav5;nAe@FCB83tq8I>H?j^9(1hIKOZlIH< z#-`7cqD1ac)U!{5>FBqyKSC@Gm`-c(u1MaIz2gIqruqS0lZ(Z1+p(9!&(YaL>On_y zl!*z+;g$?qE{6<*xdWfSN{|Urivo9V>)y1B=|wSfPuq*O^J#Zu8}>Fj4-_WKDy*3R z@sBvuVGj`6LFnGzeeO^KP*o6<)i7GlJxy89uTdog+pNi}^zUlwYPgOA0pRc;DI%(! zG70MHHe|qx)V3?>zUUva*Bc=%@ww^~a<}cl1DjAg;tx;1d;=VZa`yw*WFG-ey<*Z6 z2tFbFk=|zsYgyck7%|CKvX1lwCZ_K);NX04{Fh^zje{^ULAs9&xGkQI6EB#8c!HR_ zG{b~+*IXQXQ7I;e01P+kzgmNT#ow`-PcY5_#c?W8MEXxRCF+0hfYKOgTqXsieqH@j zqEfXZ=2Ij+$T&V!vf!VM&3n_0dS=la>06ZX7aHLnH9c*Ga#@7og$JqWF3{a=OofzF z`y->17`IDc~V=@&yn-$Sq$%pEq7Pl1uokX5z*k=Rno~|D!9n zagQ6-00@#Ob^R$omyuKq1kGRopdZ&2HJL1xv>-nllDb*Vp!PriLgE?`z;EgIjsb7{ zaVzcI%}{?<|3@c9>U??G?u4q}dxOya7lEz?8%)ZZtHe*F&Dp@o$7-6aG}A#I(uMff zIGk}5z*PL<-`uKk^8xNZ!UY8X+{76kq{719$&-@&G zy}kbM5wv{x57w;WTu2As7PO{upVKIRQ-2>FP^J_yUj%EWphGl(b9U_vV?~1K_k{8E z$+eWvfCwf)W_E@gTTTLjV2X`D5R$Ek|H|i1TjiVL*ry_zeHS`fHF>9g=H zt;R_gKz!fGhAa>OB6zh6l&Xo*}|xP!C8XnEW3K5c&KpT8ws z1{@`8!}5Seg%!$4_O75gM^}JVf-Op~rw0*!a^go=? z|5h-)7DQ!~{@~rfHCeL`w?|5%6sGyUP2kMoT@lKvyr=}pxAd{G3Kg!#5xuYx5E9jg zjN6FL3$-DaMca6D>M_&-V-^X5sj`@oxaB9IBpy~|?T>1LWR3Dsr=;v}U8cszNbWC% z66hNEXb{e}gOP9n`Ot7f{}Am;vj(7XqY9TC7M~$%K^FKRe$<5MYr+Q5h!4b35f;oGru!C!D3s;#^M_k+TJ*r5Lk z^Yb5V{$Yk5iM|$c{sGn+_5-lpDYlBw0E<$`XyDBQ;Lj z9bWO4#x`ax-TsvQjoiqxcOJl?2DD#4&?@#p@R0*BIuaGT0TIgTW>Wr6)x*HjA9^8- zw7;mKSQ>QP7FM>r)f(Ad>odhWtKVxT!`R;;ZKL$nH{!X2+m6n(3BZFHPm8||Rw)4a z#u5M?uJUenA{~Jsr^uhNgT^$vQ4OsooK$2g4QLn5bR3867ai;SJx# zQpCau6;|6g_AluW|6^nH>-+?GGApgNB;B4lrL>A4Y_TDD+b$bH7Y+8Z_iO6Q_R(A= zVtU6!y1MHWVuO;hi@}Aq){#I`wAIEX$w}e0H=b5d4!~lnqqn3S+I?=^q5!MFDQ;E$ zVK2JO!>;LUZ1fjzy|;^kfb$Qnpf^kWqArX?E3TndGlLOsxX-H7HQaon{h=HCg+%>_lnwJM`=HEUN(BFk+vu;_{GWD1 z{h2-Dhw3+ZQc{>*adzqa!TgVnl2y$U-wdTDxIpPYz)f*x0OtFbUMoK$;UgzyehZ0@ zoRv;N<^C0riQ4=2*k7!)KNTMNCNZ>L_6l-cGw=AuLyF;l0LKXL^1B*RjemnVz0R8= zVb(bQ81#i5_hV$@E5eNpjX#{JGGY*7r1b(2@fd1$?WBa?A)^j#%20p&XdH%U7Hmcb z%U`U>+iyH#?Ayy{EkPNfwr~36GhvL{bR|z8y1YyqAKX}kAQ#WB_(v@*waFn$$riIs zDZ<_7i4K5jH$uzQmQO9jKEOz?UO&h@JiLt$YbeR*E_6ciBUeSO)}$9^Sbc~n#pSl~ zir~2_`<8q?pj8$qM+<4mizL1aTDBo<7Zfb993j%R?}>bWAjej6cbPsBfdU=0v?_GU zF%R2`^IPl7j3TrQkyGz`o-XSQcFrt|Edg2P9tTooQU^<6>*0 zo^o@$=@2#1*?Kqz5pt&^${0C16zef0HXO8_arnL2hv8ghfWv zoTra(Xe?J8SPKN0h4&-%@{9SPi!Q)~*-zzHh2s=|Sai!a;)*zzlq7&4lO{r=EOIO# zoaJXO(^J3yiURfstNOTKr9sdv7_cdoyP=}5-)ZFvyRA==E|77#0?O(CT%T--9#Lk{ z?`+Rb7cJ%Cl})j1C`FO8JevMAnC;<8d;~i=&nLJbAmOUWNDl-{M+a0bjx=*R)m5*` zPs?Kle+VT>wu!bqmqQy834b`8<&bFpgz)xw>H-F|^wZtJp$XtAx-xa4^DFA=DZT{et|BV0t_j21h>pST+Mcp!C$=Hkp(HZ_^P`li-jMY|}S zExG$ag-aM0-m_A6yH4s7?hbHmRRF?|wGsTzRfP{m0V~>P;0iA^ig9rSxr(Ze{80_? zZD<0jwhSKYfuY{vfZrD2Yqj{KMpM5MF!BoOL!snf({~hs08&8EXnb{HA=I4#8DJB> zfDyisBR3rqUMD1nf|8;4-uV5mc~8phN2IgyzZecd@;6w>w*)3U9KBC}OhAgBXkt7{ zd;T4nKhM%R6EI;S9e{@&JP^#pv5N`hEgsMO#EQ|YlktD zmL5*EN(3^=5lt0qh$E0TP=6-*I|v4_q@aR+B*wdb7MjLC0o>l3+=M@R{m6y)^D(rl z^%Ahld?8c#l5*=R_ubFO^qo?O1LOiJ@`4ujDk0y`r~jT8IXO^Cmv%Ix2ECXuCvD~5 z!33yK6QJW%rOPVjU4Xc~_5q+)>o@0-0NDy8e-^>^NFzVpBELMOJr@@~7!y8USA|@E zEH!Ne1!n3S2F*7`_~a*mmIW~Id!Pyp0y`oU2&^N&{#@yJqOn#O3fBo>_R~z=1umh0 z3TpmAXvNi@Fb3?+ep1&pJgz$fk~w}2Su215h@*e^_V2mltZL@ z={HaRn{0*J?_o9=KEi$@GQk5#%KG2Tdv4#f?aEsSjo%T)aaa_soE%6|18_QN2-{%R z2=k+0`T=Xe?@4CTe=1KMPpC$QJ(oO?j`E|FblQ!40%^1Ut@Zf-Z_fz)$wyg%v^iMc zCs9ppOdOpYObxBCO14IpsB9eU6f6{1B>@3u6=x$SH#<{i8EZpxQ&eU#+o!e;Dt3m( zrp)4|E*8e7%2E$dnKdj-oSsndva_QyOPgAlKXIbqVBth%eqiC`sA%dSW@~L{Yh!BT zM8Si~ENSu7$<%>aTufEm)Y#U3%QYL9dz<>Y0r8iBcuj*g9qO@)I)CCf(7Ci{Q=^Sud#g$xr7mk9?$ z34+Cffy09N-U1>A`iKDY^gfI5=2%I0OWEc;HhH z;Byc>76SHNR#C(|N`^?3_Bd?b;i<^r2c^xp%7b61*o_=~P*Cyk2?&X(@6piSr{mz{ z;^yJy6BCz^l#-TtsG_Q-uA!-=ZERv{X8y#&($UG;#r2unbKjSK{;ysK1V%(gMaRU( z#V4etXJlq&=j7& z;nDHQx6`w$alwG#ej66>?=Q!N1&j+89v%)J>1tdsu&!4F$AU+=%Zi9Cs)S@{e}|II z8yV+8cxq`g3YcB_3$BsFASxae$7kxVS3~tUYEwWVJ|boO}D&Y`X7Bw=pJ3EmhFhKFm=%6mF$GE z55W-aU$pXJPRPriX*8Rq(I_1!OeNpn0_+5yejGERfVlv$pPj6$O9oTalw#iv@(T~c zC15~0d3Dlo9#WZoY6~ zJ)<@#=KdYB*@-Xeyl&lAe(eRP~^5Z6{@5H1RrcLt5xZd3TxH!I(FeI~+3lY9`( z%R%fG{gJ=~{rP>BNQtg}xNhN}(*)eqA^nAomuI*rRf*0%qRGd%Z(H;EGopDh>!vQgVZHOq z1yKXi&cu9)Uz2`CJb|{9iMl6p?^kP(gQjDhWE_TVOB>|cN#5gG(P$oLec6*NJbS9p zdzjW^GXIbLz-cLueTLo2Vd)sT!cN}doKfX%&SelQ=1kn_{|M(+2!^0rc2`s6AKk{e z?qA-=P|h-4DrL7vslK?2Nw1Ss_d7JHu;S52;ludR!XNgC1Asq&kjM47)!C?IGVo3; z!zK0OqTi5y~MI~3q!VfC> z?Lf{q{y~#}zwN&^lekn0B)wJq{0<_Wms4IywRxJyxjXVLHMVDF!iSt$l?GartYdR6 zkf!vjWU#F0B@-CmNFYn9yZ6AE=Dn=W29^oZw--|CvnL9h6&h-BU^Y4z!(O**GC=ln zfWV6M;3}=uBUyXE_a#`%X&6go2cIp&4>t}b(Zu}x!_h-z6R~av98n_~9yq5L(2BtW zdhUsxXtk<{&0OD&{^zMhQf5_you>3%AzGRxZOH8;v5pJcyZu5#Y=SrD=XX#sCXkZz{GFA?CfpRKh~H$ z<{8xrP266xn65S?NlIKzu3Cb+0ctUzktfr_7YCtGAZ+*BC~X%%B{h?|1ATjl?G9Sn zORLxKUQC#TYa)+$j8G(!e+tCjU(kNv`Y2XeAx|pYQ@FW4Dx(&Cbe9iAMr8 zQz@nlA+-bIDw3B-zkZ?cQ@|OePxu^~3>aSlpOQq#$)#{xY~uvqjL+&S!nt^DTzmn% z4AJyM`E8cwj|9)w%*nCkwLOS*%}~<{bfV%)~3&(aeJ2@^ULbh zZ1Ot{eh)!8fh8PZRK6WE#+^Psl`&q~(#XzR7xY9d?ISU~mvq+UfKHMJU@m8nVB(uY zjV1ixLs{1x*oR3_Qe)1eEJBRjDTqX+Tw$lXxp@F&E@l-^DNBT9lV?97rIY)sR*E4d zm{T+DC{-}YC!TI$5@ughW?%@2B|Z2f_a2o&%C$EJL5ft^@oK4Nj~{$#LYLuPD%LKq zf}F0A19->^U(>J0Db+aPO54R~QdaLXH5hT93<0jw6vJO{{yrfpXm3Mht|MI5WdMb2 zag!>LT5}o;#=G123CQR9N$&CWqH$O!|D=xh=cvn127&v;{9IXwv^iF-8meyWv6^uHyI z)DwD)7`js7;;`&$T?-cyPyR5jGPFHpevFB2dF^TU*-d|FaAoL@Jla7bb z1zv(=#GHg)&we>80bF82xIpz7I6-=HXpS~o_I{ILgv(mUQs%F>iCQ~<0VfQQ77jbZW~H1pfx zHrUD3#NKtB-gTV4oqUy4?zf{#;S#XxTy5*WzyEUu{$HIxcu)OGGW|+n)Akea5AMbB zve^_W0C1iy&Y6UNKL|1>S*4`>@q^pO)Zej4Em-7q zorAg!Gi9CzJ3CW8bgWgi<2@rAee+_Fs_eC9h2iT5&%>uGq0JbVa65nrl>tyCJH1kP zc{E(uf7t*zcD7;j0O@4yAJ*uM0HWf0y3$M>6KYLe0K~=1A0zC6*K_7d?P#h5c9N1`K85G)RQ%5$QI zMehBKHn2PuAF1 z0V&;STqj0?A_CGeN%r(c$+Udrr)(XD(!_k?!U>@oCJEV>(;64?6Qvw!?;`85X}!!Y zXtUdEb{Rm+A?VMsZ{rrY2n=#Go+5Ie1E)s$sO=6Cx3U;%aL=1NFWpqzFiGHMy%)$I zR%QVHimZ)u3YSS`)UOm?Db~mAc4pOBz_rs&$lO<|w10){9@18w`Q`Rg-B9(a{fwAJ z&BAmIb+} zrKlB}C@k^-tzxTJ(?(?HS;7Qzv5iC`nMJ?~fAj;Xpa^NWT)4)<2hOygtJ(FLC-7Yc zm`s9fc(%o`WEPQM%(Avelp{D96N6do2uLvV+7CJ38se0dbsJ~vXjj}FXi?UT#R!eV z3vlp!EJBtTD8?*RICZLC%%5>@*-vqZ35J$+{1jPqh>79p*V$ILvk*cbifZ&OcRP7%QyELzgCg zb$=iTQ#6C!w*Qr#J|{Qc6yoOy`$P}>{-LCxqSSC-dNjb>2sgsXLUnb%`!$uck=#lt zMbpabP69>tk>#-p%mPd)?8}J}+6x30a9t9=CXs~<@&{k4$R^P9$X1aAp8{oBw_6h# z&$@NXVESu}gtc9O2xpRAXA@KB`*);W5#e9tp@0SLr%DSYMt7#=OWAr!-q1A*PYI-Z zb=mC^OnKTjA!RRnts6VVa7J)xNP+V2cYsHTOlP*NtQmF-y0P)@+DC>yG>f-9jZvo%*u@B1%7*7d8YW$A#?AsLWW-ImHmBl zIthZ&msIB$39g&di55rSrq-oPdU?=vQm}p~vuK8D

    Ai@OC1i-ed;$>oEM+CZzC@ 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�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�z~H&4-rF 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�S8GvE z-`lCJ)yh(QuOFF8{$P9@rC#3zKZQ{0wMmn)`@a127hVPIrzGKA^;q|FDL9$+l(bRG zM(95GMU@)JeFd)($|fJD~ic z+hRfgdbGxfXGAB*)!lmn#$UE!N^b-y}Zmz1oz3@ zRBR(Y&-`#!x;5-^-*lQ~3(#K$Z|WurjjDgSo_T&w7p zsj`6M(zVQiuhsZF{=mWF&vTu$wPQ}feU6j>V%#kFJgyEW02bx2uRWitW8_d{=MnV^ zI&J(ps?!QN4z65pk1cE*m(}G6 zjqzYKHcl^O%z?PCZvYeOUJ*NpERESJeW?&s&8)A>>DZ7Rvva9-@p3Z`}9*KiY3 zOdSVy0!O+DUbj7VLmaIAk<{}eA4dn1=IGTD-b#kFU=dW!K02UVp(BMDHsJl+qJ0u-+40J-uBpd^NX=8Rk+~Koa~4kGz@r`4H<_N2^FNWD zMG1SXU@GrO7DJxqZtM^sRT$XE$OD&Nq|3D*5GIy$a;gOZ90)^Q#i2&Izc}>~6KL8Sl3<&S2NH3)dFq z;(iFI)wcXcyI0@4_I$RdTWnZaH@$q=jJ;e!EhF)}S7x$`MZ#XL2n;CmI1XRScwSkJ zM?0|H+KU;&Dz<3~<;G>#at?IfBuR<|2VQBL5kRp}!U=KRHHq)#DhQk7y>1A3_AKRF zfc>a}uf}9P%WKV`2*zT@-}`g_?BD(WI}iHFd(!;yBdaGsb=KbzSI!LDK7;*o2g|zC zPz88ny(hQm$2rZ{@jusTlz#wfPl3r~nDZ-8=2PAFFM`^?2-rLYSWz_CPFI@@C-SK) zx9*uom1jkcy*JO<00$~DbC}r6os=$yqL~Z$6G~MN=cd#q0*3l~h4nR_xyf zCxgp_qm>DBHT9JmEiGv!bmCoj0u%Z-c)_K_I;Ujm5rmpnoyeeRH-5nTHhBP0Ylo#!}33VJhfz+ctLe!jR+#yj7aV!;!_q@?TnWY-4HjcCbAJa*6 zDzGumWylAaQ0cvE|SWX$| zZjkZOR=hv^W_dxl#yT}!0GdIaUohu(bO8GUDh?6SrJSTsaH90Q5(BAmeR-NlA#B07 zo2Kf-8ke$CubKeC~OsVljs zlOiR**A+Oh77p=nQcrEQvGK4T7f;%%b0LcI3T$6M4fT6Dn3nlftDV4&vpR-7<;A*P zOl@)6lFZH}Sr90&%Fo?VcloQirh_5z&_2#$&o)bU4tjz(z6UGbD zMdJO?L~n-N9WhvJaD-w}Ku6cGncv=WT+dQJDrgi~Pg7V*B)cR!3iuS_YNHrvP0HkW1lg3YwI9HB#rPC?`Zcv2 zwaDm&g5&fjL5iC#nt+9*OX!ir;ny!y`u-MlHPH%nNyMKj+k*lbBmiPf+zf^7ZBJ2? zPGGXM_XpRO4WTadqF(BZDeT?YHdUDyl8D~T+to3$3m zpS?lwFM`k1BsacHFic6;H>LXNnySs-gVL&bk9yrcxz-G67;fzr88=`F!i;4U6#b~e zj9h@PntG0CxtHA+Bdo+N_O)uevYicCjbU0%2;vp>aiCh3b}$KaG{Tdx5;G&B)?NP8 zIbflObwmM~+yavruH@x#Bz#er{+O+#ly!iJHzr7EVIU)260kU1xvkYL%u!Cx$4ael zKG5ah7LvP=pqS&#T@+^7it>3InPC(hf3W5@c91c~P#kcU=E;)63vA?zq`pTwc&dQR;IfimHX`D;m38e+>I{g#WYNb6%f3kO z+xVV6O9(ewmTz%))9<17c~1al-vrBwq*-N-q_osI_q@+-X3l<-x+dnSqJZ<@#RFqO z!P1k?+MHbcdo@+PMbt5ynvijt#3ma{u7+c}zWK;bL^Mx^vYIR}3tHYgZCB}nVfR=; zcd0Hal8qVk@B5lrlZ%Rs8Os*RLMmL?VaKHb&OV6qQN1r2giS$mM#2!aLUeSrEJab2 z=P|7DkK!yW?ahaDphA%KL6+J6Bx}_*}^_*L1g>@Lu(u7Fniw^Em z?c$H|Wt_s@)LZt*q-KS%pZ7uVM!356{M6xD8S1S)v;=Wx7J_{Yly9g$GjruvvN~GX z9OqlodsQHT9=sz)oKfsJu<6#!XrnyR_PBH&;tG0YWYXE0D=}%WH>5`JD@e9U+W(Ck z^D5dJbh~+DIydGsk!v#E8#nlu5aKFUbcs1&8l#m}yvND`wq7khM0A*|@GJMY-E2Iw zYoJVmkcrIU(;;;L&f+=XfE~S66PR4=$Lh6X-%9DBeV`8aoAB_vJXC;i zaiJ|CPS9}1xq|yIPDM+Gk}-}9&Eu$sPo6T7Q?$DD?M86e<9`bphmy?pdE%Y0m{B9B zBxO<`sI|2W@$alyN64iSBAGL`)~4Yq7`uzg?aH+*;>(yvkmN~A9QD_|CZR@br#Xo7 z9Hv&g6!!9dbdHzXy!33rOM?5F`OdO88rHO{O({=$EBrN|342;XG;1j}{8mwH+Nq&ZDMnXyTBN zHWtz-b7N&q7V0Hol+(a#rd zQEwEBb!2zvr5@Q7>OaL|MdA3W(Qw`=xEhrkH_E$Hhlp0?OjYMU9NKo&55N#4?gic2 zSXSZ7A45)_Zh;_pvB>8~nCRr@#$#E8tY}tn^JMS*`%iQB?1(s}sUbKHyJ_TVEl<{}rrX=JXGC)bDVqzm&YHH7Q=Pvtt}aVNI%4e{mzjpM=sXZWBb6X+2nkJ+mG^5pL4diV)JxVuTb>%Jk!49 z(U7%DKXXecb4c_TgQjg{5~OI^?WVpmjhY~hO(?|!C1$3Ba!11#e%2Nq#9H3nxZ|s{UuzdljH2HAnHks6SNIM_+XV@ z5@b*bQZLo2IE|DiT7{|3Aj3i~WHH;OU~rJjdQ(NmK@%uu_atX3d|I-n)>(U-${S&t zp2!D4fv=4fJbyN78zBd^2RPD9O=BKD35L>hyh4}k?=jykG^0juZ3M2zhTA9vJP09x%SgdKNd%!NS zoLE`pE_cYsglzW@FoC8OawgIQDo8&as5CdQA5IE^(J^ZEU!4?nj_lc6q zfMsWWZp+;X5>7>XdRq;oQbYl20Q?6zzJV3C-Ek+lvZj1D1l0a~%)I!Ld(ESKcLiIK z3LyEPYyW2){%1b?&${>@`|$smoy<36?usUiO_=2a$^MH?Kuf&Z0oJP8pRXew!xhIf z;bz8*M0MU1b8`VWWPsrC-7MfJWdL-F4Oj@rEbr*$;iKDgpsjqBP3Flw$MwOFmi7PL zmuFJGFC17fb9ake(;)@r7VQ%q+CiMT;t*;b?g65{8x3~3WqwPv^DIIL&C6tdx^INl z66AN4f9J~a{SBbg7I%I2Qz2+A>@FL&UuSehIxP_V4~@Csqji7Ln+xO6Vb69*nEsK- z42)614;nunae?Q*UvIf zhQ+^j+|^TAC*1iI-U8!3T?V7wzYC(eq+$3`zZ?h1I7vRZiT!a05TMQezjcdbV@vPN zYw=?#09cGdRyRAuhm8V+!H)oSW70oI`Eo1s0*D>-s_#aB9;Ut)Kn3N^ogBb@>ru_J zwg2hLm^$DeMJDd90sKNs{C9)@6B>X z0Pn~Ux5}ZBBhRf(_zz|wFpL1Ta8K^z&B|AxfI25I{x4LF{~Rgd-$88n-DjwAhY}lO zh=%3o1keQ}5ciK@8f5$;Shk~dhM(C%z1X{dN0^JgDE_ta?zJ8N%AM%?uOMd_^LPg0 zd3*$Oj(+)hF|Rt5y9bO{CAVpPteb`}9}9b}Ibu+Ppm$v&)Vmv0t`3mV zq$eW^y8Ql2JZMkuUa1W4R(NF9o{D^Yo-)ciS0^8{b9u04FcsVc_;cg8Vb*wOFX^kr1gbztuxHn6_m@I7Xz_|zwlx|$|Xl`ocWeR zQfWDso5*~dKc?K*8IQv)BS8Z9-c4miOCn6Tf4cS=7p>*p7Tb9ctvP%pan1SFg$mcr z?jr+jZ>;VXCMic_os>a$bnH40svbbGekP|TZ^}}^d+VAxDJ>8~+tL(+ft3)G9B1yX zj=h}5o3Q+4_G4~K%SiKTVuMPC$ko-jc|elCRGOY3l#L)!<=z-n8uZ6s=!)V<$Srhm zWs3?(NDbMiWre&rd8pKMZJ&<+V6Zia|Bb;m4(pSFU2RaE_{MW|2mog8nz3l&PKpPh z+7SN=`Y;*if7t<;%sFYAKUt}g9v&1$k$vQs(M7Eq8PHyatQ+flzKkWFR}o4DPr+3S z2g|k4Iy#O@gP0c*S=fo1T*dlz;WauLXyFJR)_@``36ayUSL!3#mJR#Pj;pv_ov{P0 z(9;nYr7qWDLcQs``8}KLPz@Uzd+tz_Q*;r^wDnBXrTXp`Nf&~0Dx9LVh+CKOq=3|j zO8VP8R}nQsiFldTbUg{I7&T$6Xhp37F2@y{u>9^sS$y_!3DJ|om|}th)B$GHkM%GC zhaon^4`q{J#MRX+-qAy0Du$73hsK0fr}tYGi&s{nHagfE8f-rHHN^Al)caEkka~i7J)MJyn_s%H`4V-Ql^$LwV(r}(hwMJ(+1jCLAlBmuND{n<$ zs|G7KhGNT~=9i85=**~5(BidYi$q9GBjF=e{7c?UbjZ2)+lYB1hnI!VZD0p=`nUw{igbv0c? z+&4xmEjOh~V4^7lq72ZG8M#-~Dk#g%1eOFtA={O1ms4MBy8(wH7w}1>sjS}I`N#t0 zZ4vB5JlfCeHQcf8^dkvQjI6gsz-utDX>fsleBs4)qM373EEk2d(m~u=Ime&f|8hu+ ziLH$JA?5`|x{t`iw9Lr~=}2t5bK?ry5s4Ermgp3rzTSGHYZ3920r6{u>bW;;I}vu# zm+vy$??8)|MCP8Yse}92c}Pi!t(Bob=q`z7f(;z8&dr}hqdCj2O)M zam?K%fqw?o-)?(&sO=vSavVaV^}QU6O0bdG%&5qO<%`_B}JH#6}aNy z$N*)B`>A7p{QjX&p9wU61^qS+1O_+xVr6DB;gcr_65-QQq6H!*@XXe}D-bAHkS) z@8_dpSQv0Qxp5`jkLM((~| z5UkwX;NBF9@$!qlE7S`TfZ!NzPfWYXGNWKIjd(f4s!<>0UBUkT^DBcP3OQ3bVF^0o z-Clz`LUpByqqX)&k66o|J&#=WYj%|r>H={*?JT!tGGMjf%Rhc9g6l&=DYdT3!qT8m zuSBpBB(v%^p4&LVo~`M!`SsCb!zkg70dNq5OqLc!7<`+|b(-ChFT2DM6~|(@6vspSq+8)QJC! zy-g%?DraD6uUz4kEx>-cczfG*Q!4+U3AW;Ni!trcapTu>w|b|UFWUG1p<$~sg#SEY zkymHgZ1!F}Ph>5QOLXE#kL}xg6@X?{6YQ5wXdXYqWXJvvY^P|qCByLyTc07>I?Ef8 zbn~8+-!)%?EHMauNEyZbv@9L^Vg?mu3Vu10d5YBvJi!_egC_d?e3qoHHciz%NRC&! z4398(gT#we7kFP+H&RHjcMBrUcu&@$rf7a|=@qz7Xj_I`q|BCXgeAH#U~j*Dm90y@ zprU2{-N*gYhrhTm`Z#eoxwKot@P3L~$I?eL93|Ehu7gW(D)q!|2H!e0Z9e4ib9^r^ zRa4j{H=1F2i-a5)sjCwaAlFOa2Grcq412LEQq9{t8p}EdE@qR6BF%q}ww>ZJd^raf z-`#p}OPY8!otReX3Mmsax;w1NW#7}{jMp{F9(sjP22oeY#3b}Ck5M~Zt(anVnT?J}R(e{?0P^~uAR>UU$I zu^#i>gpRm%Qwj=I1SM#p04<>NIRCi?^xw|!0V*&5%w(R-{RJp!blS3I>_A|fodO=y zm*h~0p7JHeD4?lscITufeX;c`2nGnF5|aMWE6y@c{2Mpq?;H_=8CJXqaDyQFbh=`! zB|lliw^vTez91>cJjh~c@o?jKnCX5KuFUcV`JMY7BT|<5aI(?)s~4FseDY`&v(OBe zMwZ1{Cw8*XViy8Y9FDj~wywJOusz&Ba9n}sOy4kBSVvGWW zSq{dylAFwtGo3$c#2Y?~?d23RcjYdr!IfN;K;IoEVn+P=vrTfZEJ*V7iWKAYXtB|u zs;6Br+$N~^&?=JlKP=4!mooLy&5na;8t-8&EShDG1W;`aa$JjZC|8P16lI~|J^q5s zezfgGm24yD&>6&kLA-rZs48(E>CPUAs%no20rc^MCucbl`gQ%bLNv(ZPxRQQp`8czu5Y z(D>g+V3%&?1e2?4H83(#eYZr+>asRrVj{5j@+=Ge-?$b0hi2c2s8gD0R(lh*35@P? z=Uq;HMr)zr*UD0wOOb0JEgX(4vx6Z_C`S`^_jO-&g|~61)Fp4p0texJL>D!A&E1D_ zbN|H&V9zrE=hh!f36QAEAA9T7l$gpzKS7<$8I`kKayY-hA$1uHxk1Pa978W_ zC$FZ4^WbsmM3&FIoXc6CSu{HCGYBm%Qt5SU11_kj`rQ?Q&o>}@2>4$gLpV%3QeGbP z>SIhlj=82iJz{TU|5tOghrUgPP+pewZJ%^HeL08$=g;%c#>4y~fZpV_>z14{ElHx5T z(mp~-x^|ufk7t((bEVd4k&~7$KwSH(QMDqNXy;$Tt*~5ioW(AlQ@%;9cJOoG8quXh zCR}(~RP>a3emH2MSp=uMT}+E5HBk$9s?t2}rn2@xTVqovp+!c*En^maWj@xnC9(#i z{4idZPR)ECjS6QFzD@S(tI>zbMg|t_50hHYyk;u;bUNu$bxl?98kr1dA={a)6I4+e zhHB>vN{t@o&E}1|mSWn{qTF!F{w@UaY4$UAcMUC+iy8iRkJQ@UvOU7YHOHKUpYhJt;+G?2{JeTSb%&r z338)+t=ga2_Wv_>y>YtSsf-x_K1Z|2`MgR7=3LdXN#VdC-F z7K3*uh3vKHXg1_j#7-BVnmx6h7m$;FKyZs-|iHXa`hI zA+w%4JHVT0iD-F`_UW?lS^=Z6EMgbiKsZ~_SV{h-!>{U4y&{u)_brC`$>$7DZcoRR z4o4s};qr@TIeTK*J}pZwnidHPc@Q(ZjTzDU{4dL0o(`)vkpUQ=BUL425e>^iPRh!9 zV-w}6(+g%v%Om5QMlIM}f|T8Swm>8UKnk(I=K1^R{2Ij|K+K_vig#P1HmGmcAi zL)grtJDD;~e}y~A8KbkFUqNxD1pnrevwu|Z{zUot7xiY)Jn6p!Yh}bQg;)aEzv7JD zaDgBGlKHLNLCS|ZM31eRouC#Rg5I$m_{+3Xxf4?Zcefi&Gct46w^wQIqxCk}r^dEA zHlDt(6w9z8yTRMZ;^ABQsRPqK$u{NL@fz?M?=F+*6h5K^Uyg%#_0Iu`vA&%9cj8g} zdv|h`TGJRg@lqeYLl&@cyj6n}dPTW<6xzG9Ey^91*BnVPB6m1z2*?7?Mg3C4>lTPk z1a$Q?dAFW_?hx7^B_CHT)|aHq8-`dARI@z{p$4b%b9$cwy7UcYuk42g>+lzHTq417 zef!ZaGvwowysY{8Eno;9<3>P$yAW0JFuxtP(5LWn<5(toFE|dSkl<)>$#7|>Ww@Dt zXnsCo`UNW7U zHCdExh~F{nNx*J0N|1edI0+pW2g&|?VT5!lB%UYHnV8%x!k;FNOy5mB!}#q8Tx2-d zt5DCL$g^P;gDpD@ZSfwp0lAroS1+L`0ccWM^0i7XWEJe{!@Kt<(;JxLSayG<+Wf=^ zR;&E}BVrqxRQ;K2Q}@S^of$|Z#YO^<6TlQcgBZPXy7(1zt}4<0&m{p&HDGwR=kkN$ z-LFao@*2-J;o3HRl!>6L-l;sBqq_maag6Mfd<;1&L;w=`#->njy}c&OK0}7w5gP(; z@flW7H}il?5}nwRpSqMEF_D1Qie*##gqfY2rDPI8dVHaJuOeW@#r1Zb@{`H5y|vvU zJx9=d1j*CDXEA=gOlhfx}n30C`9nY?AR8muYsxwlk z_q6<6+7vIPw9_o=je@1aJE-$mH;%lodH#j=OREVN!7|abiuYq9WkanK_Nk@9WOm{6 z&+aj-TQ`wsAGP;&Qu$>}5okZs(&7&={O0iGHjk3{ zB?bF?x}y~JZ~L}LUP!zRc!Z>z6pG0%(M#>@V#r5e!0J|Byp%s8HbH)T>7(N?P?NB^ z&5Ch9$QM^m1dNrjyjIv8A?&ZS2siVAFw=JU!pdw?}kkeK;(x57bB{r z6?=^fiY_y^MwFG6(5bek)1OPV=FM|@ysJ`z)%O1XpGtAdb4}-`PsdN1^LS(nJKF9^_CBB!wzFb3PEWgG^kjBmc?DA zixpneH6v%Grd7Auj41lPp5(_c;#9PEUWIMP2E!x|LC@tdLD83D%TrFO)v1?y98YK* z_mJtjn}YWW>CpFZxU^Q27((bHH_7iYGgX$eZR!-XgbL}CsUnbKzK|v_c;SSa-%*_u z#0`zz>*<35oh&*dA1(Efth#*BXh}{y(O*o-bdMgUt$30^Ibi~MTL_`5H4dQ>1X@EXj`EUzpd?c!H z{~>E*myFSarcaK%uT;>-Lw0|`TU?;99rUDVPvFr0dzTY#@|KF6mY}9;WIT*AeecY| zmU((U=tZJcU@o4V1`$(`d>YOQ*XWueW{}acJ~tU6WYrVP;x%4XLST~6215l5Yb?XO zF+4AJP5X;^!WWQmyq}ih)X+HEH8T7h7i_J`u;UoIy3SHhEF+ z@-0i|atr^3JzT7qdyiY9R-X%+%;ivbS(eAHFWdr7xbVeFxWmV$s_%C+LKV2Tka(Px zd?~p!dq%vmt7S2<9xLb7S8mw0eiaRTk9{rNm<`f>w5>mYEUNQ9Km@3 z2I{yzQ}iWKMJQ3hiQz&oMO}tL(PsEO7I&3;=7&a6o&`c)EmX?K$wxjzip?U!X)p6U zRWt5m7Rr)WL&0UQ|`CtuH`a%)r5eR4rfOh z8f~Ahvk*4L)AnAN9CsxmiXc&0M?c=`N;90dC*DT4g5kKpcw7SOwOPQE4NpOG%Ov;p zD26027jjo3F})APV4|^A}<-KiXX_H88Il>dy=wz}WqUx1b)^Oj6JgUtn zG7@OFc0X;^{W6LaEtj3o>zQ$~8)ni5n!rMQ*=*ls^=x#z?|762A+`t)|L`!ArV6b} zkL!wn=!55{Bg{qz`Q{13O&QQktFO^t2J1FmLXd+;l}@{JK50~D3TmUsBN;6R^;9kT zR__K6lVYpM@jXj*kCfytZI!91GAtWRi*7#ffis_JM#C9z(jMJTRx%$yX*jr};wsfh z4<(plOJ3yUIlhjBUF}XzQSsc&_3!R>Zroe}+A{yf-0eSdwDhunqlwtc|6wEid2=e= zGdjir*hdu{H&HrSIDgV_AQNDu|G8WSSrF%jUc z{!r5RJBpzKa320p-C!rN$D<)Ntu!EJ-Qg{cBX=VsyP?yVn&_&ZU>}ds*s?N zh+9_|!Jt00;?+U^ z5RC&*D_pupr3%wdhTZqmb4AG2#-ObsLH_|l{KJFz??e-(2!}uOSCcr>>*SIXnFXp% zOCN`O^Z~LQv5#Iq>~feji&5dfvrK)+uD9p;&eg|x_)6_Y=44Uy%wnjgN={eJbir#b91W(x2N+vMCazp#S1LlF+%6@ zn9=ASefxbVYTd>9irk;dY5V5pAt4ijr;BK|A@#TI<$2BqhM#L?_-j7SMqE3L67OD} zsP=f>pDr}7l8;Y{d*8rm|9~x2>4~fCw!z)l5}lJ}*<3Z-B`o z(%z|p-LmQR=Z{*W^dzn$8b_-~NUq)p%EXC+qQ%&{q}*N6n=YjE;aut9gB)x*mVxyRkyDd&fX zXvyt~<+s$W0@e)33|VE^{_JbhM^G9(1Ci%6rk#nV5Nc9Sa%sY)L9#D0%j^`JL;V@C4^GvVhSwt$pl+#>OZ9^;Lm~kcO}r zt|cEQ)}n>F451OFCl5Z=?6NlGYYiN1PDMUvYrsc#t9C9EA{lw`oRX<7=wP=vJJ7Wa z0rDk(<n;`UV5gU)Mmb)nA-pyy$ zyl-+8U0ZLdo15uByL^N*_(-iu#mf86R2t_ zA?8KeMd=hPt~zeTE`KD&ia>KGKmY`Qczw1eW2?;zfMIm@h3oIY+HZN4f6lY~Z=Cs-#2cZ5)Mry#G<}q{Y0z=sLXis(^C;&F7fzc)DznSU#6LIAKD}flJxybgE zg}bC_rEWFWd;gcd4g7M3)@T`S8M6NJ(1Y1!$1d&7vq?wL!$FFc z3%YHm!9pNNs3^P7@!-XWQXYcWB;_OU3#v~r4umb6%-*KcX2i07)Gj`+sw{@NCw-IW zyt5!huWxn!&h(X;NE~mC{08`FZgg$Dl2xu&Bpc?vM;PrJmHURv96b0_A1oqpQj3hf9iX-U(%TS?8`Qe}d&0 zs|8h%(i+bj$l0Kk3n1WYvMc??R`r8M@)0AVKE|Oc8F!pFM1PCfnE*$I7`gAO+I?Gz zq8DF~pP)D1GkAv@6t(!W!^hw6O0!}7kW@2afVx z50Ei?v9J(Yh-F1&)SL;2BU%+$Y`{hg?^)jhVl5!>)mrm;!RvlNNm(e_rXXYM*>$KE zu~8NXe7(WvS#7R++VpXX;`4Yn6^|^>J2Jm2hFFRIUipcSP1kItLytTcJp_>%NEoU} z`b7hH1@b?MhVPVx(dd8h$gpbDHKGre|8ACfgw{!!vE)-AbN^$mHEpVEjWAv5&P6t8E$DCjm70^%}EE2V+Nb>d^gmS_e4TgfqL}jh9wKBtmH(8tp!I*(u(Z)B~M9C zj3Wf(5Q&#>FQz%~->Tcu3~i}&t1)$|a!8lamr;~E3K!>2s}rckA0 z+IL#pN1Jc^Vr9a4T|}PY&lxvg2KJ)1gt7cj-BtnXGd9=#_gx5^%1m>I`GBqzI5vmMjgIl!LXhAKrOyAdoxJ;R`xDx%;C;PH2*gz(V0`WC(Kx-;LMZ`E|GqJz= zZAtMU`AGs?LfFsM#$u9BFzY@qCeBh1!usMS3rrD#2#lAJv^Nvn4w}LK;I8#_U~fL3V`b{Oz7C>M%djWU-4Y#}N_wgDGR9X6`vb4G$h!-5 z`9{kM$;4M+w$lODBG55AN#aev)+i zn>^ruo|FEMbZ{}Vpcs(4_3t>PQ~O=b=Z~xj>d!e1!ryI5p#Qcsr%HRXopn-@KrN?g zLV)P7g>7Y#w4)66EdH4Q%OktlRb4@vo_LOpf;K%eth3YYr2bac$=^GINcQ zOcbpM@?*xD`-Il7vae;_w#!cgqB4k3)y;ni`#b$q}{QlWbroU$^ zeyR+X7yk<~mh&5}+uBqbkVpJuu0pl&&%iOXCs2IGc_@V{fyMi15arf;CpXl|R~Bt! zL+{}`vz()onhu)8<#ieQ3gkTKafDv@oX!$ogCsk*BXWyJX5=!hBDM%EAp$7-WKe&b&oUZv2a4PkHsBcJlAQBb))p|+a_xw7IhgWZ;9BJ zmUiQ28(5HS@SE$jS|IIq!DGMMwDkJV>(l_lQroDlOEdG!V7`(5MF?bwBAM}@Vu z7wd#Ul?)^I>?)O>2ft*>tm8u0WxME}%#$Lc`R9&wt#$L`IS1a0t8&ew2F|jASE(JE z9A_vsv#q4M`PXrexo7u$cF=q$q%3r4TPN>zvo+T@^l^XnuzHE>8Nkiym$pq&su3AU zI{a26&f-~n{k351A(TjBH{r{}07>!Ze$%hxA*yxpoEoahrp}=3ASMb9DpqYTT)5bQ zO;t&ZWD*1A+bu@EI&rVzlw7<$ZIj;Z zOau3Vo+9P*B9o@ZD3s47b#9NkKpB#%v!uO?&6ORdV>R_WbzAdPLe=j@s+%^5x4%&( zaqlZPw1mk*m>N9~P|0t)!gjyJjh1uk4~AywlpV&K;GjevA0Q%Eon4(}4LqmC-j&Q3r5|zRLq*>NGQviyk3|tY@=T@X}b%5RkbU`nS%T5+*(} zo$Qkg1KDK51!c{Vqosz-fw(gk_h+6bLE{0z*+E*|}Yj4(FquO1an6N2SNl6WS2-#1W&0387r2O-6yA*RaThm|w3J zb-P6SdzVbC1e!PUmpoqHiy%$}lKI{c<7@SYYR5z2epCtEW{Al5UcqUEo&7t0o}$e; zL)dt_dfnLFgmqk>ft`kklkj71y9{qDs3ZqI%gnz?DZi#%v{8wCoJX3UZ9OT6=#Mmg zEbQtm;BtJ7u}yKGckkScN+geHK2HK6w0)HJa_PbIu_9|Fdk0cCq59WDF7Lr+fEI}F zIaQLhOa9!FQmwKRdA=l>AreZOtLu}SEX!P=$*m)PGpbvwy|HJ#jLT# zrG<5}O7<}TLd&E*?98Zy0{lEnwN857um-UmWp?DKugHV@c>foB zZvhwOmpzORAuT1{3QB{7Gz=Zm-Abo)i!h*qf;5s6Qqn06N{DnwNDM74DMQEbfAG6s z#a-S1?!LRfcwgtEe4d%-*17lGbM8H9^GK{SI4d~kQkaha&`!rm&PB6@bG5x5f`gezWa|eGi2Ao3-GU3+h zfQqUJ;zn}PqcxP;QCAUJEnZ9ofeOk}N2PWqFG)K}%!EBA8^YWf{S%M043Q6zr5U(9oZwQ|kS29G_2B_H>O3Fmw*oB0?5Y~Rf0puj3D}V}oy46m zlo-5`Qau&}GBrFpWIs^ddvpc@qi)~bN^f%BC6rl5h5O>n18FItI43969gyZ{Kem6h z8juf!f30a$;~Si|7k=dVwPW^QRQOL1h|2f=1P>VD6oAY5Z*Nco${tvFTwI_WPNz`} z&(Kc;g|rcNt~Z&ncCN;8-;{d%kMm#&x^u+LvA1L0Xnyv>^A(0O@>6o^WW_&%k$n}6 z{M;z%V`NTDCIb>#NFIIn;VAam&zE2I)Y>W4=|R68FMIo)(=V@Jot zn>Or0XnA5xoFWh-fu$8aZ57sTlpEKb#jpf&k#Ql2mx%T%_g+5py!QA?6s>5@NuC~M zp^>)Iq9mh}xqW5-$~I?PWG%g7VXreUk5?b0dp9$XJhyl~dHoK_th>9V8ULZKBTw|{ z=0GJw!JEW4J^YiwoMRJ(EoFKJnd0||b+1js5JHM+Q9*8fw~IwWV9YHe67AjLWQP#W z*AKCkypUX+{C5j>1V;7IkP|*VI^*P_&g?qBFyKjqNC9-D4_Ev(p zv+AZoEYlaK1yR2qkMHHh7rk1oo+1W_FUD73EyJDr3|r7jQ&qgnp;@Bz?l`C)T(f^1 zlIm%i%Na=Y+4S)SHJo3j9P?WCo?>6J8-DLII{`a)j)iz4XVJrI8eX-_@=h6E+wE8f z{-c$RsW)#3C-M03uK?0G8%{a=2^~ZIf-qX_Mb5NvBtf0X9xHXVw@s!zl$>`fvOd0V z?%<5!` za;nk^GSiDXr^kRCI)IIFkaUUheNf$nY)<)oFuiyf!`7 z9V3rZjt`{nWGj9B)^%4v<8B6MqeQaLKuB2`-@EO<{jMhY6vzp5lzM_48@?6~UK%SrMT0l>XShYiTw@qG+g-yryh8~hIRzrD(H zjFhPBZ^4`F>|2Xnlc z)mUc?f(IKKHIFCFmY|uI@KiR$2$?5x;;wK7Oag;c#9F(Z4XKot=E~6gNk(oV=6rQS zsM?%#ogle0r!tuURRUHSA|IXOeSF5NfmZm);gU6oC~T>3Ly}u?vmEH={nC z654Zncc)E+Q?@wOrdsGCQ2S@Or@WKAA6o*a(wPapv&XGYn?M)25X#jy8Cv}r!JV{? zJ~ulRUmllhLYGN5E`e~^Zr}>l<}4HiP2%M4ll$3hAyKV(<$?=_b^7KrcW;`dCIv~u z<-k1mQ}wFmZ=1^Y1~|5dZ4hQX4XIooJ6-x})6br*GVp0?bsy?e`{KJyU;P>Wtc4nO5KGs7m%(aRcs2)8V zj^Lvq(9uOi{XcyEpQVsb{pL8FcYoo<)XD%0cy(~;+2j+47Bk;l&gV`Cf9UIm) z@os*2cE;ZtTow=(+5HdKduU$(==*@LsBNt4=@dI-+9u34;DOI}J^S)JTc>n}) zEq(n~@JIfj0n>j*H224~;NqS2RgF=Iig{_HNIBq97BFrlqJQOdiW97-*5*hqM?E!% zckG8;2$H142B&Nz1VE(LyPBa*lR4i^T(>Jo0qoCgYF_ zlEk+{vfoQ&_lsWsvk(K8uR~;@fa}sqk3cZU*d2-!3kWhiLc)iimrkGz$m~})p1X~G zFLm`ok#n!HKTBnXCR7m_)^Fh$m;&~h%KzSAr2kV%Np4}Qg)<~T8jSC95+LCLobJDc zDRnu{XqXDG_{lnhw z45Zip%D3YZffnyms(1TNhCWFKvIW{e{uyNaj;p{B0V?^3rva<8!4{TCj5I zABF2hO?ke;E|qKOysC3N1DtQKr9aC6B4i%nW$qv|%QJtcRi@5*b?yNHo#nziz|r4 zo=7_bkr~3yK)&E}Dif-mdTdTOw>m*ufa9(aN|R_zpsi89(;tf2}qQTxOaF4 zYM=(_Xlq0lCub}}rw$@wgcsj#-tgt>PTejJng~(k2N_j|D_Yl=8#aN=-Ha1bwG8Pw zfMWM=bV&b-_s!p{^WW;3GhQE>S#=e~@OEwSesF}-dXmZ4h-txz(NFEy&FpdW+fw(! z22kg-x3j(alW-4c(@-)K7n`lRweiI@PM4P&Pd?^78Ff`Y+yHOg0;166ePA*8i7YA) zV%!g2nt{v!VXMAq_&kvW?h*dLJ)#&0|D|%N?^;gWa9>n73UFd4lsWI=07$I~ zWW>@0{g3Y8yF5)(xnwyNw)K~J5qXym=>tSv=*q|Zuek50auDD>T49f;Yzi!Y0zY*7a7335`#q)#+uI2v>yZqC<48idKWNDj6F6T`iM!&Gm)Cuge zKCqaFk*>9kQay@PWUfOR9rw`rP* z34IdDEk>!$=0nAAN1TGzyJQv-Yz9K-LGH6WC`+KzPiXX857KM;RSixCWoSwhZUrU~ zK@wF=nxjJf9IHuJ`h-rI$bHmy)1u_q?V6Z`TJV{L-|8T`-@7D=yySGx$a{VPGx=I= zsZjyCdLU>=*byS&W-=!DCM;(-?y}2O_w9v4=e8~bQ!AwHoPgFCJDcs5b^M7#PfPK1 zsSYGgd~AN&>;A%4B?i;!H8~qu)$-Q#Z$J|69ZS;7osF%shrtL#b1zp{dw8Zb-dLGM z$#*&4E{=aFWm(k)QtI)`0VQn!X@egm6&SQBOSwRxigD`sc9qz(_FqUR#M3a7>%6P( zC25pSOerIz%x*P%UTNnjB1mOP$>8q*-{zNb@eN+28QQ#qZn%rdH*LfcesdDj9u(pQ zp;r^J8|5n$=2`2tbMc^ckVBKBHE2gdsB=E4_jc={C}6Wul_I45nJ)g{`P+Z~c)4tw zav@&nhvDaNf?Fj3XfJ^c{Wn@G#Q&e)Fjjs>vNC=aOM?L6Z_j=|`uSes)EU+pBPqeU zdAn5oFic7$7!(Yh64cp~BuAc)HEBszlLPG9{;JO3oG1T%@9~=c467EE@h$4w^{;7R zE}E$RU^uFI!B_boNkHs+tM7tY|MDNXF9A=|561t(A@7pwKdxy4{Y~7dywAjHdabYc zwn=8%$Hu7vJI^V=f$4F4rT5C0ho`@fljijz_%Xgk`!q;i2f9jaRdoiwprm-Mr{`UG2GW^PAl zjy~A_xbJWl74TdHpGbk^7I{y;M8ocW$>?@CoTFcCm{Z-X2ab2RkZIr^+M@av2KN*L zA{q6af$&7}y+VP2gJvfqCsr_WxM#;<@xhSyCV1&F_5KNP{~>U=P*Vjt#T3#<3I;Au zWI{l$h!jp=Tlg)i3w@JcsEI%yKMiYD?t!$1b2Kc4qyjd^p)wcymcCH&_n?2z^l$X| zy%E3Bo!`{szk^O6iy!XOQuk&|djU*=29QH8oq?Wa_$-}lJp#N9QsLM9ZiMo%aYaWd z4FSjZ2|=Tb1!uQ~+TT=0|4>!Ez#IK~&u zx6RAMVKO&JhlO(oTTGa#=NDGg z!F3b(uEws$!HL0f{t9^H$KXGI5E1!aiSI%sUJTy9etlm4&o()Ec!6HN7GYQ8Py~%*4SfEy%$w&dtOutS%xhds|6aiCs|3 zKvO|qT2V>*svD=N|+_B;-pdsA%XIm_UIFTo57x5)vXZ(xpqt$iQ14;5`T#?-Kr14l$G~ zs>Z0-oCrAmBh%1m#Y>wA)%v&SxJ;ZMqGJ#dlaP{Kzd_Hy$i#h%hnJ6EKtfVVT1Hmx zwz`IJ0~}< z>~(oXWmR=eZS%X9_pNR19i0P%Lm!7nM#rGDpXTNl7MGS+K7ZMU?dT?!-W7s z`U5QB?@!^v1K>hLMn*zLJ%I&JmgDPIZ*J$R8ftcu3Y2vM0Qhs;ntqSn=8#m}H1@4!MZWIHFH~+L41T zP4cHug)C9QKB^sEfBJ8bGaO4&!~U3BNmlcz;4iXu zeWVh7gTsmK>4cGzHQs^#n(0hR2>ea+mIS)~;Gb30j$*vV7sVqnf>r%$4jS?7t`%Tl zX9t9N{*bYb3+XFtI6Wf7t1`yobU9wZvf-E}1&ob5BMT=cdXQzexfL{KA$smcir=PI zi!v-V zG221mU$Q)>c%aLj?_Oh(k=Z)3{B(IVZ8Ip#A6}*G9w3R8SV*@cf9)UZ=zU*3kr?(^ zj$on1WOPrFYs=?UG10q9hs zR0VP*r*i4t=x@J4b^-N>Dif5*ND*2cHFkDiaotl zw4oy5oVE&(-uDHi{?<0TNAL0 z0n?cMWz{{vZgSv!Oa1Qcf5uE&t^@0KzG=U%+aHuH-}Eb%q?Vtr+gIxQb=|&_!ookW z#P=La{z6yJvE(aVt^A6Uy2k*P{6S>CV#!z9kT?1jCx7?$FV3Xgl{PNBu-=IXPKf(U zgbkZm;XH5kQZ{mULip)r>p~qO*$}s?QtB;+Gf-=yfZYx9hbT&8ktBgYEMopo<-XT)l; za6IH#6SkSkVt>BA>BI}~P#cgMd?NH*iY*n^rVJ2;NJ!{SJlcCBmf8Z29MoyoXEOVHfh~Tw7eRg<#Oa-t$ECocIh*&EIxFb z!W*zjTzxedWmzGC&xnB{&u3uC_t_`Z5Yt(pjTjI}Dt{>uPUTV>Cs!+Evd%Z;A{VP1 zx5IybZ*RDN!7pM_&LG6z;aycjNuh1rL+)~zZey61yGFo7WEF)Svae?@&jh584mqT* zfS)risztoC0H1#{kc5tr5=iT!h9WTAWF>omsFpZJ%erwV@x=ivS^0(CNmk{YM9 zL#$#cl#@XLugi`++*yu$^c9K!^Y+lw4DQBw{Q$u&*)~cU1C8;E>&F8KpM?O!sfHCe z_om47$j%eU>gE}UGtVbc{?VSk1IxYcu#$RNeJ4JInhoXbTa01qY_g9fQ89AHUvJ3c z9U`dxbWlTlT_u4%uKkpeZFH%YRh%>+KZ_11*K}BeUhrI}XE&GJ4ZN4JTHDEwAG1w3 zVmpf3Gx_{#%Jchd5tKfe#r4)b#BJXj5NskAe`iC%u>_hU%_nkjK!%#Ml(Fz^ zDMpz%g~YU}CM8h`488ajoZhEToGVJ7tsObbX2#!JP@_U+B3W;h7Or*MOCvS&#OHCB zU5`QMS-d_$vO}(c{RWgT_^YNwJU&SB3Kls$h1mt(}dDY=+eh&db9CJuti`jK?l*>T(YL zZdn!0q;vrmk$o|K0SQiFFDcAS>9dqC)c=lU;!A6n^u01F^gT6tSMP0dZx`Ui(T6ZF z!|=U!<_h@=oTqLz=@NORtEoQ!go}Dq(s-$nf2Hr1>Gu{c;AynF%`j!F*PS8Y-|4Sb zCq>R3w~rYb&J0cc>4;?L>OlYg_h#Ry#fA3EAR97&P=PVfo%f>#EBc$l>m3vVKUaDA7v& z8#ba{A|O)*!;r@f%!fs6>31hzUVSu=`;zL}x>lCljgU6havZ$YD}5@&6j;XoGfpa} z5801Uf$PJJE?~8}TwaIPJ#>d*Ej^HbKnq`x(%%mhfRsZOSnL^kl6wtssRShad$*tK zh?DT{xs}omuY!+-MLwg{E5^A;DDlQ!Dk7AnFTyCvb&A$1tsu>FBS=6PW(9(e>0DFB zTb!LY@S1RKTIL6s_!6gtS?#I+;Jihk{8N2fERqhVnEke5SRwVAx5E(%aNY4Q1e|Lb z_m1xI3}$Yv40o|uGPSO{;}l^=s?G9zJOkZuXHjIxHrwcO@At#9iUXQ$>i&MsmVLBX!Ykm){50cC`+qd zqCCDbP#h*LLo*^I1a>29Y0M0JWGz)2h4t_$`otq)^;i6T6%Np5nu(NG{HKT01fDyNCltr6Vb)m@b4v44xL;_4UUr3)wqn| z^#}Rd$pcrM#dEBQLC-I*ZlD&^hS|y!hFfP&LZuwa8@Hu6Yc4%S_7cP4TTMzyjj{Xm zg0A>_vy_13%)`&!4^UE4mN_&;$**>b<>djBTatp8vBJ~^RlRr%}V-MTP>(2D&6IG&zGgqXa-2Dmflsf zC%$-QhveA7*DF%Jw4yAnt*m)C+i?*&hXQGK9BeLGUtX{MP;mqmU$2+Zgodn7MDVWxBRVO@vJVc%E1Usj(a%9 zNviqc76Th|s!>}dNXSd!qUd118!wP0h;oU>64%MPI-qygvHd`k*b;c7`?-ln{X)t6 zD(p~>`9)EoZongDBxhw+N5H49>t@tIC$o0YuvF#t^WzkJ9h3 za7qVGzWqol7hj4(QODcS){O%5bKjw=2U$zy>c?00v8Yh?BG&tvG28WlpDZaGig!d# zIH_vq2~-z`)BLBGPRODb**7S z6YC}~9(9tF`?a?tn>nL@iXd*yFzcra^04oqN0ZEd!rzLp*+1;3K-Xza_ zaXX5HaYDe@}5aFIoYD@h_AHt z&9>de0y!@`idFl;7B*537O6%Hs+VqSwl}8uUj+`0wNUBnyC4+~sWoRiQskN~ZS1u9 z4lVD<$7|X}&U*5`r&Z7iLXc7oF?y7okraaH$qlAqhuUb3AR==~#$A!7!zD#I!AnV2 zUeM;yf%-;!Si7O04oIdw7$}Q{p@pzdFb9ZymX=Ymbs-of3}_}@cyhUxPMf|G+vy}! ziYm*R=CRk8g4eBdVZ@FU54i?!$*5PB2s|&lJ9e7=fT1+CROr}IU+6K78zoHB^^{5) zPnH&T&4D%SIB15UJq_{^FL1$`^AgRiLN|H2;Yqzu1l0*hXbKq~w0v#q+D-r^Cago(t~Rmm;sX@}bOXq|KXyK}+IUldSGLe#%VGa$rg5S*w6gDe={g9%LhNP{_) z-;=!j{qpN1ysWpDG`8yu(%C{MrG3k#RjT1e5PU68kz7z(6hwh@Z@`Fs%nSzaU-X4p z@SK5wbWqMvinvJjMRMVF9RRu(mk!K)3AS`||)x|Hu zB`>2KQT0OSVx^EvLwF~}_CxVPo7Gf_JHfDR@D?=?csk?zW1cipbk{@SUTd{Qd3QvU zJK#H?DqQun6?(K3$KuX2-hzRv69SN6THuE-5g<|82&-|O7kGy~o)ol2AGqg89a&xx2V}kWTrt$D zs=c9a0_5+!Z@93$7neGgXTm+nzEzA8ZI|G*!h-JCE2BO1R5PSJUi964%S~2O&%oV6 zw#-U#mGSo5wMh@MBQDwAj?QIMC##6zgHhsq$Ab$fp)*_?uv?FJYt4KAa(9|zd-lfs zPRCB+Gux*{8Pk}7lx>baXzRCKvo7yZFtvJPE>IsHQXk*|iNO26KMuXHE)!@)us;Qn z-gteB{QHXwWj0T&0KyMQ@WYo2#roj@Vu}#o-ERb{ zgOqi5k5Mw?@S-y8NfwE;M; zWh9DuMRh2ozN2kL*?==pJ>otY$nvM$N6jwhx<}J?35ajPt78Z0=%X~7QhQMEP3?#? z&@YI5&dpjNI_Df5v0?Uh)xdZOu@Om7TVe*eZ^qH^z`Gvi4Pz`re!+WpeoDqaE2!}h z%5yYQDjKkKDTXE{vz8ErT-}iKa>G~;}W9iKV^dhXP|GKDf~Md z{D64jq;ui1;X7l>_|DuwDHl&?KOEKAzcZ(c3=Z_496Rz|)9^l6{Q`_5bQ;iOtbM-7 zp98&cE%f!DjvRL<`99x$$6(Hnn)VXiwdt8CSL&2pFGp(9UV5A&aEQKpY)uawsEhdR zTU9>Z1NW`*iIhcfa5h%#|Vm&mO%0#Yac_f5`D8cY0H%;Vwjv_ zgu)B&dy>GUvKmgVgyRpWI+&K0^%t(A9D?u9C6&zh63THO-jebjb}VE(Bzz)Cd5MS& zHp4owNj81{#GhO|NxiS`!U{F;hcBqPNyHX+_3*8`7C#qzaG&QMkvD50GWwfpi@e0x3 z${+X1XSDAzE#ufZl^usp8@xoZrXQp7@oJk#v1tv>J}uLwi?V>pu-NMY`uLP{s|Vn3+~O8a+$jBT;$zm|3teouZsg__enKs%n`?(9%(!O*eCzS z0XOG+rY#de-=ul6wb-^?>4~E_NLi)@%XY5mNU$8YyBY2M=$*7%UL=tGa2U6IzU)(_ z_BKjq8{t9k5qN=x^+vnijWbXzCrISULuOMu)0NWlGOpA^n;AG#((uN#&43`qt_Y7K z{6^Sf=V_*eWmWXjod(7YAh@{B2ks#D4ar8IbcGGJnL}A0B`?z>&17ddZ@tdE%C(?G ze!<8H{a%XwV6Dj1!{ z{YnRY590&;c_7{~?Ev8|qLeA?YoT;)vHTOLCO%x6u9Kpbh1Lvgqzee6k6=hm`9yjP zbyJ0}(7Y%grfN;N9GTR7)T-TZy>*#=`1@a3@+zi9#QLXd2GxfqZo7LRQdmM6ixXn28j>0}Ku3`IwHj7x2 zNyk;HHD_Ud>S62dxy(G%2_wCqMJx(^AK9H~ zX7t**PwN*$(>c?uBTPv;fe`Y-!r@tWo@fkiT(dTMTN%skL@uCEK-kf!2I$cF_KUn# z1}Y@`RgrlHg^2K>Le{jDGXc=9ZD*zFy3L`v7agyr<+(?q$4BY5(P(kwtWD9$V<)5X zJRY=BIEpR}DuViJhSZ>5kwf<$PE6cBE-g2kF`c$`S=Zb-b)T}8w`QN3YW=hT<@R49 zwa{V?=;d`;7W}BYzF2dtv<#>w73+1vRK8%i^_4;=Jq6o}Z)Iyq^si*w>OX@n~(-# z8Uoo`(iU&7aiL?`Inm8u{!qzV-37_Fe>?3@saMVARZ+-lhreFI%we-$YQa_1>L5DS zBt-TAn)R3%WlDeCwHnQAJ5dv-9BYZR_a@G*7?;euGC6%dRZNq5d{up(Da;GW$xEXS~Py~W3m$Pfs*#mIHoUq1Pwiu>PqC>?bEHM zZp0zHyQvAP0;Nbx`!HP^`H;^%YM9l#fPQc4&LJRW%lxY`Etp>JNF zhLK(^_jYo8ks`&XQc!~gnu!t?o!`xK_oc2YcPJl+>gaV)e_HI-Es3F!f5$r1`0>wKuE z1tBg2IR*zsQ|Rl3V+FH`mT`_Xb$FK~eG*VnS(L^6lc+rNs-~!i?C+&lhDllVl&Fzy z$co=~)(w8yl0fI!hD5bksQ7ZC!UNig)SV)2xK{ z7>O+JPTl0Y>NWqUjdEog8cXH6by_>#Q>6x@PpZpM9V+QVTGb`%^=F*~F53ihLY$%{ za(HhOhd^23C$EwD1jCWBOsCQXE*~b&bsckS9Yb|yxwxOv9{O*4+%QG8N7FK2;_K5u z>0H&*FTkped4U*!e5yfuycE`tdFP#3M5vzwMYx$-4l7%GKZY5f=grtv$OCr*ws9L^rXBuZd@b$%xC)CSjMSjzeX}dfN61!y~A>IDN8s0$bge z_<9AkZjC&tz#Jc=@)|L=7L8kX=27jB5q*L{Oo~KHHv{XDgJjuwtlJ6=4|iOzt-qr{ z_IxX!*>34R4Qy_cKIOP#o1SO4>j&9<2knqmpw0eM3`JD#&kOX$*UV-@Or1;U5F)%K zcoJCK#?}T#yZ440-dO9&;*9UH_U=bOrFZI40PhDvsQ@p6_IZjxmuIe7y4LoYYm*1eKbi~lxc13 zx#1oVf{u0>-l`;Qb8Nk@-aS$xMXr2#$~0{LIrh^)4wzA7hXpEX@mp&I`@R+)E~xO( z2+`i&XXVWl*HP&`6_=D_mz6$b7!Ux~PWMSdX7xis+1W5v=RpK=R2EcW@RK619@9`v zO+axGi$74!DMQ&Eu}Qvg&nq}9ZZmhT2yI2Nd~5wQE}D~E*TVl@rt|Fu{lym#=kd|- z!yJ{wc6)B}W)E|ZTg^nxsgwt?p%_~xYsd;~ME9u<4o97XTI0E49!z5TC#V6aBGnF^ zHMXnd>zs^6fs^)2Lq;6c8gcioXSz4nwrV?Ua=-|VYc^Z!V~IVkyg9kqI)Zn{K8*RH zsxBUuX%arag^}a^+>?JD4g$a^X(IK z+zRDnHg>y_VHLZ!fLti>m_;CDiFD@6x=z_LaaE`f>j0D`PNmzQaI85_r;!wh3oVf% z%$K3EwcZ`q=K%_Z(SEw>#ah+bLDGBB>Joos^eAQY<>kTLQPu&yiuICemw>08b>ksh zT+waZJ?xsVfjlkp`~#{Kq2vLMwoEfx{=d2jVBNokV&H$$9;GvMO` zrFihcvX;^9hJh0LZyc{)vJR^?bP(2vOY=ikVh6V_8h$zfprE#rzvvs zPt4;Ey`%N^bt1F1@0d-gvJ35;kwvticq{T_lrxYiJyp35c5|?#VIDEvjOmTo2*5K= z^zZ(+`0TN6PZyLvU$i`$8godBI{0O7C}cQBYlu~A`(rxa<&WrV(@Q(I2nmwN!WN`Q zhTebJV6$UQpufwp6o=bfl!*|gBrVO?i{f-Jw6B83Zb)EE;ehmEKUYZ5#3-QXhP3Wo zro6cr6-X}Wk?BCz+p8O#{7?>cF*z|8?5w2ZkkzW2=358 zp{ClgNIYB?J^EB)mx&cXVM>$FxIP-Zd$WHrq_Adwt|-Zyh~0Ov)S@y0Tid-c0ckX4 zXPJ_HO-no;ACXkq^h0=WUfZS5NCa1;7E+MjI-?H0F>DcV^D~^T3zB1*>{EkWF&LHo zM9aI*!v|H?AR`F99@xfjMVa1LBIFxiD6ezr{b%No^vm=2Q$8loE>m*P(gjQOt}~)m zDpS8=wrP1>-7)9*UQymfkZMwJJ3n+pfpL$hTFO(+-9if#f@UU+GmxWWEu;22cw0+e zl@CT6jOqDQgLVN+SB{?YzS~~U-H|9#q8>I9Ivg(MQa%ug86||QeXxRb=M;R*GgAN; z_fnaJ8Z=_kkf>>n8vC{7vjlilrCW4${BBr-n`wMmjB_qU^Zk11&N)X3V<52M( zQ7`Fh_AFmfX5DRoZfJS6XwyHDHkY8&N4hu1M_IR6d%L-V*EB;(XT0k3dFUq~fa{Oh zw^wyM&X*APk0mVZ?EcGgDI%yV0oQxezv#%I!TLA{`{3PS<EesH4XkzEs+_#CWx7hx}%fryFC084G}!Bh5T0Ail`Iglb~|tyZ$I;LSt%ci zSb-T-dlyG>^mcFCYX7QU?#JlsYO*RXL7+y0I_tNZeJ3aHfq7gy13k;QSjR0gEw#WJ zJ*SnFysKJlTZE7H!?7lb+Zz`-4NBXF0GYd@#@8*9#>dR zoWbx!dXr;mbmUTUUPB-XglXzSx$I7S{A;f&P8Yf-4JCVp;9pJ??ImL0G%_2u5LTe(K>NkSPNJFdin>vNGv1W|2HJ|8Sph0?eVHD54wh6);cr z!acn0R^2bF$;Xf#>sHuC>Or3p(_ebpMhV&>6oER#0P6?f#z zX%-i4%tu%6!wU3FqGngq8XZ20`z})n=9H)P_G<;@>mz z=f3(q6TfEyU|T=X+W!Df2pvM++TdKISwC=1P{OOoK+ME&f?pF>kUO+{h&x z2w>=S_+iHXJLR|(X|afIQ8E7daN>$U7=9E8}3?s)kefZekE0Vp0d}llRMKnSxVeVg>1HU#q9054xcbBLdibq zJNKgWmA3CAbWAr#JQBLPGJ&^S?&i(G9f=Z?jO0w?T%h5cH~Q{o)1essqc)UewwOhm zFVlykV^#CxBbYR+sYljF-XG(wv+S)Ws=B9P8VZaP0tM=7rxMJH*u< zsFxwAER}~a%F!bX-+CSMAcn7X+nFPU_ax8J>BDgO{m_DFFUG5!hoN|ip%IN{^QQg} z`B&PPA~kMotrgl=ez}t;zL#t3Oh(x%X|SE`2$f|W@v!Gx>=qD|%CF31Lx>eMjePJH zOJajz=^>6_O|LQ~&l|RDg-b#ASVDJPCiG>TY#bwmoAbN214&R*T*6M73Nzu*QG;P6U44d%rMfT(ok&qT-JMW8)jF*@L`m`csA*#9M*?Nx~0{%`c?}Nj)X|LfRChcg3md~13WEk8129xJAi8#~WC7=-jr+w|oLES@U z+hd;T;}PnEEA&7FZe0<;5G~vqGI{qjv6c*)+WfeZ31R_wM%l21ACo+|im z8yp#E_q}Fq;Nk|R)MY28Rvd+tiaf;KFa54qz<(>12xNK0vE(lz|@WWgb}OamtAQ> zX6XhXOKXtn%Wwk754FaE7mC-$A)5w}8HLmnP3kegr+lg!xPT^r5AyN>(asHE;9yhu zfx}h?9AA+h?%Q^{H9ad8Z}6{l$M706w&6A4b^gW)@aN!!e9`!ELi37G=n|DrPQ#C9 z8=Aup~=3ok#ZU8up98TQ`*9OyL)Xta%1{91XZ_q?Q8wk@oWiahY5u)_zt`_a%JS>&{(JxZNiKiyzduR- zZ~A$GnfyKeSBPmFZ~qB@ytC};Ms{}Tj>u4;#6@=U19#V2eup=SCz?&_pCyF4&Ujn28xF&orNIf1)4I5BC{sNq`u83aw zJA6+Ae6gs^-olfUooM(8!0lT;AW#3D>BYHNwEwim_zWZr_|P=qL%wh$@Lpic7f%ur zKbHFq8{z+sb(6F8y7Vdbq3FS90nSsgdDq-!B-WQI$cT%_*UCE|2|WRMLlN>!JaAJ~YB z$X|3;NqJ0N2-XTzl>ThZM%~3QZ|~9!8w9QzYRkhpgHjYDsy!4ebXpjVZI4niT}5Kn zTZ^bKW9xRf=r4M{e!i+v`z9F53K)8XOzCq-c06p^K#yo^;9ot--TLf%Z|gK#&?Dris9!OY3| z$x}f2k7rL8jL8sF64u+zG33W_(&Lye&B8BiTYeBTsB{EeRLVy18$5k%p--9>;I#b> zv#5g&OK+A=_Zm(Vska`jOv9#N@Z-Di8|Am?y>2~YmWh=uoR@Ja?Ibq-VCJ$J9C)mj z(h6vRADaluB4_uptlv-;bva5JHt4I#iZdxbp*iw6*$0Asza2RP1!cf-eFWj&Q;=y} zV9O3JjW`g=LHTC!BtL|PWjjrwKKK%zCA|CMqbbf2IFVgz91=V3DD6_^akn*jVfqLj z|7L*tKx}98aMKIOty~OH3z;9=&jx(m4eCikB<(e`E=lTYYrtvbLvUIKEk-tlUen1T z=8a`_U}Xi-&4kRW)z^iKfqw~YL06$cR{v|f_j^HixEz|Ar01ji?or>4Q%hMCc2EwR z2%L%{3e99F^b?q&UQpWJV0Q&vv(QA60lKmY-mCyFgI2(ao*K|artGKgLjffm_av7_ z{dT&afLD=)dz5o8NPS<177*t!Zhj(fd)6JuXNuUjjw}#Mx6z798>}7YxG(uvE!?`R zvgH;~7|_$K?0SbGVTh}U6G7;UHvPzkUF=eK7(F9}GO3Cv`Khy4+w6hni=bljem8KH zC~XVQLP_HU4^`6(>SV#LYVYjW!_dTH6PfJBh}!J~ zQt}5Sj@TSSr?x=QCm^*V+VhsT#)C+Qcf{A|9j1DH_h%0VoVBVC4cDA}fy({ot+={) z-v4+tfk>|Yz)o$ZQ^(68rO^~Dkyz?BJIRAutztjkp285msl7Ob3ib%GoaW2!4zO@e z`Br_~(_5+RioB28wZ2BC=ox$m1*r z`#j&>f6T-1PQP8<)z#hA)xWB$oqbElEHbLVzaT|os?{X!HZ^JI_-zTyWczZkW5CS% zxVp2@l_WBgPD`sxi%dqPJyD*ix3fISQE|F{1)2 zmyKe?ESHWfDgvgiZ|uAQ?$k}L!0r%o8pNFM`$AN@OuB&-X}{BFTc=|klSk<9wx z5iMTJ7iWFB(#(B$I4#Tm!BE)d%S1-DMB;@t4~+iocSPhM)brg`PIWb(;P!CshB~Rb zFcT=wG5E>0-r!MI^1A7vQ|pc#WvR1R|ELFMtvvJ%K5UOji#qwlapBcEC3C%QNm%dD z)J&XQGE?VWo=<2C?iHTa0egwX=U@Q~D6c{#Zf|s*hklu3+tS%t{w?4-Or0;j6#gU9 zh&Dz{v0WyVfJ}xadY{nfbQb$&PxBa3Xq0W<%KGI5iL}aX=Ykyp{!tpM(@;<3<@G^p z>y<}wQhAilL)OXAR&8AcN)Q&8wF8@Y0}G|z2J?L-PYPL*=k02pAyrrTr{>Du&7?lX z#d0~E_n#s^D_FGP$L@W5a8Gw6g@8ITM&f}SxtCVyUN;a&tg1N*SRi>*Fi+=qO4&M^ zXHp`8EW@kF!bcq5g+7%@<9UT2AeF#knK}wn05*=1y9BD+i?|bDS1MIgD|Oxr7N3a( zEMHa-x*rQKtUny)rEv8S*dKKAGM}xOsr~ds=E4keI&0Je`Ih)eiu4kLCj8F_;y=-t z_{um{r-%QyWTiiS>CXJK9pOKB#50)pfKZC+r$;;x3mIT*$i4+kkM9e|)k+;JfD`9W z&k7@xGoobIew7kZ*(lh8{DWfjeiFia7z9hJoU zR1Sg%8k*f0hsX<_@vQ4T=nt{-LPJMAG&TbfvIfrE zF;K-*#aL@wL%JqvZ?=F?G1jAbshg40A6;Renz1*eh>UjUj$oQP__gLFpMN%YZZ!dZ z*OY)iQA6_$?@Qil@c=Yi7axXlomeJxoIpXAu7SgAZHhsYL^4?&TVs|Q@t{;^pO~eJ z!hCJjmUBdlyk}N~m%{SlOyj1^!a8a1TgN|St7BbmKUWW180nGSOMH9O>nBU^yrTbc zh&p)wXd#(}HL44F0s$`J{nl*NK2ILCQt8d1(lU-%YK*Iv+M_&l6bU3D0r}*wSqn`~ zcaQLG4+dFQ^;*V0S3vfv+e;Uzo}MpQ%GFF3!TMaAsExmNPcs8Upd0yZiUF|h0Vju! z@ZY??we}uav2e>t)w(!C?tk;#;4?6gVn#?QMc^1(ttOdz&GCg}$o>h$k<9euPV5;A z7)CG#83uqjKV0n1Wo}t=zJt;MtPS8m-J_U#ONuA7DhpY0P=g)PJBFI)$kCNcv6g{^s@m$_7diYHT@Qspm`Q0l&hU%y! z3FZe#$q)B=X3^oxu)wJ%uH&FZ1-H=qtxAmojvP*NGBo<- zt?*x4MmhSRd+@2K`~?$jlyKi$4=lr2ZKUw}Azr=dMnV(4_dT@h(S~vV%Nn?X_r8Pv zj{o^LvVmZMUY5%LOlD@E2$FQF8K(XBNabBbVo5uF(6RhgAb;gzRgg1xXhx&>ublXc z;AK6IOg1N&0Sx9p3DU0}*ry>6U1Ka6`nC37J79&gZb>5L2Khy*#ee3M*T0$b|4-(y zlWR%kY;|lf_@e|# zN8yisaYId+Q6#Xfq4(GaG2YcFyt-X=rZVom_t7zz9231>ipaEV2A5V^j=+%kIS^rs z^4#c#tF7n6r^9Z;exb70u^>{*vqU-lbD(dFCPdStY2Dza^PET2$GvCX1M29A= z_1wx*`^0eHU0hG}>P!bvZW15C=w{=spM#%pxioA~k->MqoLl*c)m=Vk$8bbYs2mKKI zGB=`qxV(-SFC6#8Jly%egZ`w*_)`muBE8@qwGQ2@BvanIHYF*T5{eItXh-N&>$y^P z7WPtK4>aCNXuc~3Y*wN}cPGs&G8ffbx0m9V0GKDhe5Q9yKMN|!%Kaq<=E^H>j`igt z>fqT=)o0#3J%xFt-E2U<tyy1Qoa0R#;f7&~fNx=1g0eOZUXzP?c(bV#eolXu6cmrBIiVkQp<=@$uaz zg5c0MwY`;TgOda6?;x9JkRd?Nef{v8+CN6-sCh+CZSlzpm#hV74bfM2kbCs)r#Ic` zTbrF+S*|;?;g(pfL+qP>*W@IEcITRL+GI0x+3s_a-ESJt4j8jfHt5BrodU!kkN)v? z?%#AcNu-n{%v8$Ftz~|i0_{vJhFqsy;0ihkiY1TWOm*35{n2HB)06+Y&lR6%a6$PZ zWkw0Qrs4qthF>02w1c>#vdp|ZVfbDGZbTxDiI6|}ix2s;|Jn-AIxCnXR^(&nyW5D* zS95~3KHN{L6A=E8e;Y>mW0VXBu>Y8M0YA?>qFWYrvO06`q+q$bE;9WSQ3$&2r3&6> zuzF;k1>KRO%dlx`Y86173Lw7pH^hx*S6YSRecg`Uu3uvb0!*&%h&+9r?QV%OMecO% z`rAPF)#v{-M*o;SKSajDyM!%$qh4SZx7J5@0B0WUnI?Kmr&DhPHkG_+vM>hFbn(Aw zI_)#|2@5hw!+}A!lvs?``UBVihgK*iK0;;SO@;`h~x_n*rn(d|DM zD`0jOwb#F&W{zE7rg)2RCeXU0WHU`rX0SD*l24hM@tcf-b^e_4;Xg62@5Vp?#fduf zC^x*6+=mCFPVRwj6!$FyA~aKASEdqJvQB zyjuv>j77vkc1bq*V|LX3&FqM|nC46w=xHG%V+cL#w3g(mqSx$WM{6NZv-KSLeNF@B z_CIC%7nVgVJQidQKZWK=6Z@bfhLt3k*>-+@TK8z~s;A4=>-xw0uTKtu3yHsZUt=N5 zDU)gN7Poan4&i!CX*B`o0hzMoJSz1%_U|2!b20kYJnORAzLY78Dw-AtJ99We0r}4Q zm5ada!18ea+ri3wne#GOB)ME$SXlt2S#cuxccJcU{@1ffi<$94b&qn?@ZSagpWp)l zppbwV;i9R1#5p`#x$EEd>ES<86S;jEjpaOQefI z&-h^?g(P}aD_czhJeWW@TsVlBnEX_E#G|1R0N|aB%1>}gYmIki$z67eT>|kZf@N2N zMXsDOEK4R3L{-kkQ?$;?-Nw5@hQino`7!wo<;eytq8-VO1RW%=PM-*+X0o#I&y0YR zEMxbOiCo~Ispuvwq zO;L6xfg?{&n@W7Gmc?1*M3eClPSY6sOUy*zb0A6JgJ1GS8m@!src zr|aXVi9@&k+1I78w=54^0h{*z!<#|?Os)-WlHcKu`O3Ejy9V@Knt_s(&+T3yO!_w;UyG7mC6`6zpu5{&Haq*9P( zl>~cLyCd^Q{Tdf#2M;aTI~tgC|4LEU9s57~A#->#7C0GDi^|+x0#IDSvcy%BEL7e5 zLdn{jemM`ZjqEonABbPJ6xk?qX-ijSj5RVg1B&P)m_q{Q8{)7c5_foEcl>ZKN*@!r zv#c9i@g=I1mrPQ{{kEM+G8R|aNF@Gcb3M!;-zk;Uf-Q#gud*AbY}hsA~q&kk32H_dZYMG zWD!1lIx>A3LPv(Z`K?LHzm~?}tcf+5slzqKgfF*qXx~cfO<^ld-5QALx6HD3gQgt! zWbT&%I;_u2?;5JZ@WGIF)-DAb@(en2BX;hT0B3CCEE{tN9!u-)Hyp?u$dJzHUyFCu z=|ze*Wxre7&EYiNQ%* z8>`4IpHx$kws0hQF;p!!iw95eu#tfpBw=KXCV;>63zH~UENJxnc4<#U26MqqU;Otn zgFR}(N-zft27HNFqUweoe;f0e!@lg=l9_uB`*yqC^r-({C8BB+XnUiTC35f;SnM1@ zj|4lmZ9_|381$IactMm)7#ND*hVCCccqaO#-{h%?*Hij;{3KMYy6RZMmZcOf8d7*N zv|&vIF3EQ5L1xIuyzmA9~hA zvApk?InDhDjBqw|a~7RBp)Pw&f9?MR3m$#>LSj6IgYcgC8)K-)R(Lk8o0MdK0$t4e zIcTW<^%(3IE_($e;8`tyK3w1w97-TOSU(=3ur{x03AMk04vdw!znw0=4?Fx z#r+G9Tx9C>Vj2JX^eWEDxuS!>=$GjSHJ=yM12P)_Ohkt3fV`+&O6TWwATz-oOB={; z{L3WlZwzE%Rnz)qJU0#7V*Vp-7xtCjK71|6&s-LabKEcwW^Vp{w*5|wysz$mr;UdC zfCX-f{yxctbqF@;OekR z84nGs2&g8K2vkB4Ty1(qiYI!P<>K+s6@DN>q>}nx^jL5d$!eO@`?df9+LzndYpn$G zELLF?^M0$ZKJ=nWY0a*gT<43;llJN!zRo%&U8OZmAeI4hIE!~+^Rn|UmO&P-?ZqNVynW_l6waC%gXTd;EK~~6MrHDd-J>lT zDPuXL89iETkEX0akXt8U{h~)1tA%*-Eqdpyq;9SXihBeocn}k`&iOZ5DpG<(Op6II zlFqaZ{S_0v`%rV-)ZI`#xEXqd9IoORYQ@?oSOiM4>U0woGVKlJY!#@EK1&;hm_G1= zj_a~J3@|^&>w7R8gu&S*sa)c`DLA|MBA2KH(Y_3E(b=7U5nM{nSVI!%^-6kfEQSM32DwSrHm+>iz4qn!C))#>!@keu^Y7%Y(D6 z60h;88fXS^ni|atcuFD=Ilbky;GcX3TK0htK^iEKc9p;=Qt~qw&g) zwta*1n5lPpzs)R9pv%~q!(%}2Q83@nD0dAO$?+9eF#Bi~9#K{&$KWw+CL2pC=- z*vY^eAHss=N5)$CbbP9i$jl zm7Uf9)#kqZuY&95!oJ1-FDJu$2ykURbjG_lm1<~!79m44(CFv6`U$I^rD`!yiFTCH z5K`*UcBLjYb$xLw9z(FD;AU;{O&lf)xCFUEF07}WO!;*{eT)nQ8tG zQf)q;eRy}z#u|ctfBK!if`E=o37h~3Gs@d{eOwI4tGL0x33$+`xIxB>$5Tb>$tbVX z(z?_7$1LjKEm6K6>1fQkKoC!{j%jVg^#Bq(eppHb{9pe3_RWE_cg}evd!wb25;jeBF<`Ab$V=QJuz<0(gyeK(2D3-PM_7($B z@7ZXAsiqyTNA3c#ivf%R>xv{o=vg{u+Ma;-3s_s=Wc4fq1qI|V4faVp)zr3clYmij z5E*sn>O37?{GadTV}AVLhfN*)?&JW2nA9t7hBN}LRrJ+1BYfZmxmXqCdE2f|v4e5b z(t14`Nn0V4w#HP11kcQ&>ARD~iJ1O4xZnt67=Do zEo&9nkG|HtbVswYVQYUA{3JW*Ssnflm=sXA3_zqh8?jJ?vo}2;@-gOlD@pD!vd%wA z+dedH_8qw2kmshzLDDTyhz}R@qq{%)Jz4zkfBvU=pfzfkHCmK1U0H>y7r!1|<@^FZ z_a9^Nv`Y22fvGO}?P#A;WNkDP+eh=oGG?Ax8ViH+cKM>1=w|;s9eG60xzm;G>SIjO z-o=ehA7a>^w47KbqSIKhkiOLDB3d!T9izKk4)rFBvxQk0M|Nk9XMZ>;QBzB$ma{I3 zai77749N;kB7K>15Bu?sQiw(^XH&EsIc{OVJ=$Od^T)0`OXZ@BpP!}Owxd)C+0+s#I3UivYg45BtzLCK(~|5Eh`P=xXDyc z%dI?+mR%5{H)^IZv*ptsp(3ZmmjwSgbw^|^cn8DrwMVd@Exq&nFHDIqU8G&ru=q~<=WvX%b4jO-W=N9q<1os=f`vG9r zi3q5@bB?~sw*2Ndvo*FT3&;HhBacGY-KgE|9KMSEJ81m@@SZ^y%u0n8F1n?7g@Nd$ zlx%)3q01sx6{LdqcKIrfoEjYAf2$(4jNS;(Hk2YVb-yUGzo*)QIS+1`fSf#(QgYBI zCVLaOY!f9q&7;I%qQ~nYXjJ?}Yp&jQoGW-*GyM?>H7}Dvv7zC`YVDhk5;TK51Zf#E zbB2hgH?1Xmu1MrR;d`{U z4=)T6hk4o=E~a>nycFgZ<<);fD)J6|5%jwIty3(cGf3m~^~}8Bl_#ld>PH|FCH2c- z{zotijVRg zUDvJW*QKL}clcG?z;lNG_`I5q#u|Us{5$9+&=+QYffVrthfPMfktO{;?8Hj;)>hgz zlqFGSl4qm)ezC#Y6d+?Ii&^$q)?~~Wy4M5iheNh&*G}qB>67E&ir>+-b}B%g))M4} zcp4FA%SgF_JTWIAxU(wW*>Xq~4v`76BnX)Zi$y!lU#VaX;-`7lN>AB%m*pO$S|&$? zpd&L7@27djG?XoNkS$QkX4Tf!>aph#De!b8JmvsFg|L|YtcaeoVa`I5%d0k4Sp&&F zA8G{8l}#FdssoY(U5o|HgX_XgInD_(mG|+?2E`1q)lalAeM~W4L<@JR(aWx1Q1#}< z$$mO5y^ZJZ5^;W>t9v`p?nux|yR}42FrxqLu`BCwyBvpD7p<{HcA^S1L$r-p2U|6= z?phgT)%?==COXUYiJ<$4Lo8X|eunPbh)3w}R4L*PvGdPGNF#RcGk0vv8i>Fz%kfs_ z;E-)}N~jZ!`Rn6+Emv^Am;dh7Ml6*88veaZfz~a5ojhj~HEGMhV!Fz)ru@~Dq<%NF z2uHNFT!LJw#~v;eF-5g_mVNKt$uSdS51wk%XXHQ5&uVxXm$SIaIHF)12{Hiwo+wr2F}&}9TOVKU}9uxC`%M6EhMaSDw>w$`1O4PMIxNxy)5kr51H z{P~jyu0LhZ_U}n}U4|Xs^Vas7@>~>zuOu}Krf61Zwcbh`Uu$qbLJHN8`q zvBQsMl_&YOY?)1n5j`TMHO`A#^=;K7W?F%{oe$(89xe~6meMM3hJKqzotWzW&_35f z0v?+!R)v$o|F3;C&y21RroV%xnjqJxg)u5pz*;04e)r=_K0VkQX}n%ajWki=fr4AD zy`xA^4FF7e36i6{^|FYp)(jgJG;vK>P(`}>u)g--li;EMzDEE1i^v1IgV@@sBLgMw zE602c()+nihjlof+R;Bi6Z#>|^lJAXvz|oOWxZPVjB3Z8;PDV(&xQI9}#!r|CXTGD6z30cJyCy;@K)^dOPZ&hHswL5dj;$@` z>&i2yzw1@ehne8I5&1^js328J)K)p}fgu-yi<8K(ByB16i}`fcbVh_IfpyaKMBa82 zzFuMKSxx0*Gh4}&UhWAY(@*vza(sa%Y11%$0xckmGtf#-8DHGM*oZ-boz*=e1lx6y5!y;EnW1PFa=3Vsw4og=7#SK` zbjbx5i{h681jP2sD6p=dNmjq;s#?c7fU#AG~9C**CmrF+GIu`7V>JHw3%@ z1m%QZst->;e1xo()HdWCMRL%l*hBt2mK9XsGA0szUQ9+tRiWa}kkGu(brnAs)?`X~ zZXf)5ASC7o88afl#JmP*%?3$Vq@({Y?T`xSf$;|<({NN8sSx?#JxKePC0M=O?PyLd zV3}vQDZI0h_^J!23&d(t_@KXjw7de)x0j8}TQAqXgNn78F-ot@f%MjAJaEs%uh0S6 zHvr-&3`ZsXd=e1i1c2NE&AXv#*Y`E-#n+~Q>z6l>?d)`1UY+{I3UHC_G7Ca2Ik3p1 zmrH@nI~?#i;?-MkE1V@&oxM*yvSlav7AX9|o&}8JgH_wPZb){b6x4Rxs&N*ryM8t- z?f1K|$}{Cyie6O6#hUsGQ0#hstgEVNn7c@Nl{0twy8T}G;M=_ehJx}yZx@1#=3Fuq zg}^to?k}cs;D|wyhq_O76At#-!zKz0o4&nndS|&6V@?7xop#%7EcDX%Qk81LeMA-o zmZ-nYCmft%;4W@b;M}E_Acl!re~6;%sDySvQya#f$GXP#N%$>xs7*P+lb&P#qxsC0QD!*E2k~ zntT>JPCq#&VNNvRK5@`!(iSMHDfQK6!b*;Uoyh`xd#5HdZB25-ttERdEo7}(elKcr zkzWRi8WZWpqzNjx#Kf zql_NwUp$2Ud1d3N!AxpRB3#GJK^K4be-&nV%$bebmeM)7?uN%#Mz zp*^}_H_*_E<9i-TJ$LmoU%jGS$#A|j`>CWub{nSC8$4+=jc!2_QJ?CeR~7jDWi-{5 ztd7s^a$N31J@dN_ZY*mb8#?D{p{Ik8K@Uv#q)o_lOeE*S7dgna{>jn*L z94Xs4D$8o(`%lXKo8UR%V3aZ5%kNiCp7FdY4-4huJj+?Hz5l-L5lytj1U1M03v_uz z7mvty;tQ>!4eN>e+bdCC`nt4Lge-QSIi0wJ`tG~qXx{Bz@XmHw+2ONcx*gLhrF7@P zCtnBKSY0UHZ=a0aq@^t&j(Z{2hJSUAWr(F_%pWM}_tim^YQS(?&;4PJEJ`kwd?9bL z;|~8N>L^OZCnkrR&URDBZE?~Ar6#&eQ%v)W?;vgY(A=@nMBDvD{;=W|Ca}FLBgep3 z85bF`<|)Sw#>GWzrmZYKsz%-T?5@OIwp#G|?o{xWolg&`l{+61=5_KZW7_50N`%Wx zZ2A2H;=TQhgdFH1Z9-*(lmzisi4q;ly~51+hOm{?-6x8xCo9jy&1~atYbMw%_@cF% zpn7)C70lF7W@%j+>%PA6Olz64A8sFG9Uvvqd~T38k5ZnaVrl32^>nU-C3nG6qTQSO zQOujpE#3>>ao0lB((q)l7Lp^q$I0iS*q`_|`D+t*pM*YlV8>nUdND_LyvH}Cwcc)l z=V7%OFiZ7fAzblV_1Ee%;{}z)wXE#y&&#bo%;96izzAAU&N5}oPO}@Upk93MY)l(i zEF;JHkILC1(`;b`Q+KIUTyi4um)#uuRpx;oMR(Zt?e--JkEBrTWy9dNPBN1eP}pk) zzB%Y+)lASntZ6m%lRS8geCIa#9TfQ1t--EgfQGGOhMT|wE;;oT#kUS4KJ6SqFyHWA zWUbJZDWHE85HB7#?qwUmb>=Z_3#>x9e z@_QXs=q5C%t#3`Za9;b*jV4{N7-stnU}0RvpL8w4E~ggL0D*CGwz_kplg$iwj}16J*b0zgHQe&-j)09eADt z*&V$7Jwq)~+)&h}2v{$~UIrwtnvxhVD(hHCbIyrMh+0<(aoq68xj3;#NeAaGv4g4o zPi~t|GD|#Fi|u6cX-V7pISchWXa{w}*+|fcJC)t-y1>nz4<_v_KHbnn(e|jFGAvR^ z!_5m(%63u7YmI&>cWtQC3nApXO@c}wn%Z-oKo<&hSwru~DkL;jbcD)S8Kmasc%Dv% zA*hw+)6DZcQ|!=|3d;i7VLCyt4usJCZ=9C~Vjr^gM@I0mfbn86W}t%L66Ue#_EiZ5 zpA)SQr}L&%fxXO4o+7d){ihA>pfKfOd}Kr{5NvyQ0DVDnDLqiXR(ECts`~+6(lL{k zL!CR>QI>q!Yc(-(zo{Tq(OCf;3(sT5umMVSH$IX9MPH8}W`Fq#=K)6+93d@faW6F` zh!|ECy_04Y*qeH?`b2P^SmqQ<0BkSpN`5n3L>jg548uA2L97)yAsR7B%U2I-cd8T# z-dAbys|gwNg@b9h)4VlTcM4FNyZgnRUdBRYHYRPD-bUWuN4%fJ!j)=L5Q z707NhQ3-*4gzQ!sU6W4o19s?t0%!s6p>x1cY6$EC!;>|+M<3aOFZaNot1VXKW$X8o z^Sh=%FzHlcGp;&J;qkcRxjvA7y1YG^+m@Sm&1te_uI)upWKQIF5L@-bo3B(y#h~%i zb=WQB67UEo6}b{`rn)!z!~Lr+HvMk z^vy8<+hn-4d3XaH(TD65VAp{F>TFQ_bWR-r=zat=v;T9df3y+A-s?0*4yTtk(*^Ug zHo6=a2OzzG#+o`CcXy8cO#^-pi*^FfLHL1pyIUW-xCmAE=scvWq!@@2_8l~$pl%hR zF99t>ITbj?OjuNDu6>s5+93nH|4-ZZgXn+g&6-4bJR15mZ{C|R&z#MwP{pXI8qVJ3 z_R&~9cs1u-<>iu$JCD2Q3yxNU3j50jL|*O0x4zzYoUVc%(UwniXxVf2Be}zQ>(EK1JB~YTQMU1p9`G3xx#P!3J0m=0%?8%&TmJx-HMP*1q{d?nWe7NFi%CSzbZZJk7`Y;`ermSiMq zEYRAfc#0^3I`5>3_ZzkYlB~{-FMch4lIure8dLbuHnB%t1X6)`jexhWX~CL7%%PFWODML zD^g823PLO^j{WuBK+PxL15a4 z(FMd;zZu?N09V#5CZ~di(pzqsgmpbXtW6HLSs#>qHaYlEg;fB0)MW)mi9FPxFw}f& zqyL7J8a2qU9YK%E(*T60V)`$uKgB{ZeP*GJixpo5^sne+t?vDlb+EP8xb z-%S$$Y5m+*V%2EyG{i1n#_{A0esHAyJ+R#@`Z)?>gil6gv8#)Q9Cl#!@E?kqP@44O{Rcp^tGxNqO$cQBRoG<0iex zPIBzCWjVo&>|yio7Udod(Qgb%63oK!aLyFU*~4kSkMhV{F!wIt?R)CRWLe}XW4udW za$o6rcTvvC?3xgaolANYT!1=)(%;`wP97m( zK&nyn2!|usLd$S80_8wjR=EBgCC5`UZ6=cS0oVg<{-L)d%U<2(D{3behm3sa_R$A) zx&*B_+jQKUl_=!{QMnyx)!9911M5c1paR*NKJa_G2W~YJCK6ljY5=~Lw7`3^CdINF z0}Z3T0u7~vv-PW&>a`+!*Xv5+T5eQlc)u3^Ing0c75s2JoPrP-jo* z8K4UoeN|+=Z(|fI&FZ+D(kRJ_dL8P+bMYy0P$@2eZZRp7X7}5496SAt5U4BK;=MUqk!q{-Rr& zleVwjd|juUaLroe(kIr2EKYs21=?A?DG;V#W98Cav@B1DfsnyqOP z%l>QveK@&q;k>eGA5MY+e2br_<+t#sJoltv(*VR^cnCdbx&JFi7W zKZ$%EJRPRA%D|qdWsZ}^<9ymmRuhsK!x=(p1S|7F8G{Saa(ovkW8@Ao_872Zdw&i* z)au)pmC0=?N6~TMR9z=fo@l ztf;h3X{(}gnG)mgcD2oz;pGrM(oITf)46e?_<#MJeym(KQOk0d$|MUl4+D0Ilx40p zL6p>G%?c}Ad9uMoD>Hk{uqtvpFgZv(=px&Wo}vD9JyFL|dST8-m6(qUS+@H?i4;R! zu)5F7_3q4pJSjMSAyT#lCExB-*W;yKIljWpB8ZtB38}2VBK$sFkih!Z$35YR_dM%X zi7&PiN%TS-ls>f}8vAjc1WS);Wi8q>w3@jYlu*PGGlT@W@|awLfQA z(HMcOW19k=TY&1eO;e&JN(7U*5I;Ypg84C>5vgwg=0yv;#pEe>XF-eiZk-6F>Tq^_ z-`;ht?LQ5_oZD5(_96LPWq_E4mEDU>=8G750tYChI-(TgM?-9@`rTsV;pIt426_Fj zKwAg?)0(lD!t4z7!QU2SNZZX{v!idEh^fs9p05^u(BaQ3fvAq#7mr}4(tMEF{16%Q z7+ww?c~sYz!%=23haX6hT8qIg|4c68k(%3WWA7QsP_OVv@4jPwwd`OXoEthC0_|G# zXc_y%1$A7zf-YKzsMDs%bZyXcXPh_V$+)RPBfMh+v{p{9{3Kb)o`+cJ!7(fNNz3+# zgkmvnm~0B6vk>Xsa4k5*&nfN_&)M-~&ZnA+#@`?ezs#Fbt0!_vunc0gP@F>}>Ys&c z)nrLN!cxG!BdDSn`1&&%W@u)~aNdBe?vu8*>)oDh6gh#j^dqKmmr#}?J?!LiyOI@pKzK(ux!$spltt%fMSV8(Q0 zIr>RWwFa4O?(p^-4ML0iK^qd;pLevo8OGALZkdIrH`&O`?)Cfn)ptsEsrVpSDmghh zINj?Q$0kSZVxUfrkTH3+K0GVG_q1LQY+tV0%d6M!B4T~nYN*LHFE$uhCzkTb#)4lD z*N~X2tQ-8Xs83%vcl_acw9mGE8~qH4<7oVoCqqS7U z8`%eF4{FWo8HC-oqIa}jvdPRTva_wm!)H9fgxilyj7q|{!jvO7MfGJ4J%dk?J4_$> z`?HhpU&B_g)*Al|;Q1M*L-u!f!}=QERAbw9EboLCo+(Ub6<@vLb@N#{buK`yp^;)-8rNMC9EKgV-*RPt_pj;(Ql>S>&}}EdC_=pqq^p3?jyS2V z*cf{NmOah@4Lubd7nZ=FTo1XK=ZJBAzLaUJ$f~K0?du%FO_i$HBr7-p6gOGn%AQ*k zc@rC=`(z&K)X!Z=d-=R4Z5@fr81h7zuCD43I@k zzd^De&Xq>4;B}CryS+zKx}po}>g}?Sp-zT6C9)Fn=4Avw{$W)jA?eH{Mj`w7`ewOA zoE5-FvHxk}&y@j?VzU3y^q zlh54Mfz&V*WM7u9f+y|?$ivvZ_}rDwEyi&3Od=Lpq+$66S9MemwWvd*P>9b_cA(b9 zgJI-zjw2DlZwPg3OM8i(xrgrUkEY#41(;}a(qwQ|JSOZA9K_;s3QZdu(0~pk9WiW~A3ne2lUJM?3o*(O580XyVpIw)Pwb$Il#$C5~im3>OLz_lPQs z%0$+HyO*T9Uu#Gd(JOwmryaXjlt zx_&3{>&6*#=dfxCb(MiJFy-f}1%3rX(UXknJj0Js^R#avRWn;B_%knKlQ>0NZuoYH zslzwP+7M1HsoI^owp1j>qwJoduACB>+X_9L1?!)wW5ueII7uGav$YP~+-pyGuJM-V0hs9tv;KoY3C$bGRUHM@rA0Qf++e-`~ zOsgG)s84*(#d~f+D{B~sMuvlYl1YmYy7rm5V_;sp<4I$Scstla?VQ@!hgZ>xy%Jos z{ZY64ISF>yM$N&!vDCM?9qhSTcb^}1%9d$Z(1v&@Hj`kQw*T%hT3Na4!mik<+ zkmWfHn8e#(4I-=X@{k5pZD>){a@(PgR3tB_zQy9e78NK6l(o|A5N&yZAlsc!If-;S z9_8ZORhA2}m?x2HGnBUfP%?R<+z)C)at9PB!82f!Pr8u?yW^vlEE+?S!8qtOs3M+Q zX~%gEuI4p$uiQ8oP3eL;4Y-X@kcGTXziI5BT~WUxS1#(=bm)K0;WqCygmRi|FfB6m z%(|ZdJFZ^dEG4YYcnkU&&?lj=vFe#Rp112^enjl3yD^(+Q0W2vlY+Ml`BrR4OpcJv{oVskob$tXg7)CRpK^6f~{-3mq{r5cbvL@LAsw$s6Q;JLgoy@q0o5+aDD6U3U&1AW3 z8hi{}FpqqqOE*Q_y3n%hG&C7gRW^D2#auwa!vBHi7C9`1X`3`EOn$neWCzX`%ft~s z)i;U*yS#PXb6nkq@XM@T#zXqCra*F0>CTY>AiwyU8xsF98;R!uQji80Wy`LSWLkqO_X8#2qYB9AgPfx6{4w zm3H=w9wPVBoM6QP<~8Oom-s(l=>KniXHEq`KGa@R*O%cySQ`T8{O#rwlyV@1*(>16 z5mzO>Ar7GQ7~KW19*V72!fX%F3c=XpdJB6A=Nz1Qff0U<4n!=(0q$BG z#99G6jimn2%_?FXDAh+k?SGg|)Du12$Z@W+R+LZbH%MJ~NDktML6C0?RKIjC z9j%Bpvn(JZAo-&+kdJ%*f^q^Y+}dPDq3lW!T7c+e%S#G{xL zKmqg;PNI);wb_!YK6y*C(Yl-V)T&}ko`%fk(fg24Bl5AzbUU~ph4uGk??&BMH$ zt~om(yBgKaH^zUr<==eyx4is&9{qcL`9Ephwa?9)V@uA9$_l56fzua#wSQjR+tN7v z{N1FL?>Qj7n3gSc6JLG)2jE*nAAmna-%Y@(Dy55E@Ni>MTd4Alj&eIc z8U~R^h!^YPF3wk1$@eDB#54A^P;ORK>x|RX^5e|cCT2d+0xaYps@=q?MyaR;K>J5I zMxiNCK<6^{lNq1r$U+Je+IJ{gOL@i0(KZr@=;KN;nN>3;MCOrVB1s!XzmS(JlAN3x zn9fTXiRnld5M-xq+-{cYG>g~ z7kN=g0M{}vA&9W#f2GOwb@FR@6PJt-cSSi=>q=IA!3Da|y{l$0`{=@iPci38naeA1 z=#*#fMu4`??Zt_rG5LLg*|jS?sR(Pm`U$plx+lX;L0?OWBh2B9xnyiDvV_7S-dOU- zt`Z7Ad8ak2lH(Gb&6b7c_yBj4{yCF2?MjHBQ>s5$fLvoqr8f0yraid5oL$M$&*L{w zc~X3T)7AZUVS13}yL&2%F^_@ZVlLODpj=|WfSYV|-l=kj##BpnyjuuqX+Be*hzUa) z6%#WKg&Q)_xn-ga!_+Z`(heSY-ugnA;jkMcGioiKkp-X(M~A|yCECcyD^SFWd&J)t zlLcTC5}TPpI5iL1o+nN|iR5SJ_EWK!vchnsaj$tR_C)-&NBl5SCKCJhMCE2(3-x8v z!t z=xTL*bOCSU{M)RvJ1avX3-uG=(2yzz+^GkP1VOf?8b1oXXu#l# z`-7_5`ePtwKs4-%z`a*RWip&eRQxHsUw`&L9^-oe=anHK#ldGO!;|Q-^UNQmLeeXl z{(MgPM@MvC|ET*%p^R?8xNodo-fY;2W3Fg2z4ddK)i4zwd z*E1b%sp?QIv}FJCrB1@*$jfpR;2hWc*C{uZl?4sd?O_cqzMi-xQl2cc=6z%J|GVxi zhkgPB0YwDGr>TG)=3i*|D|nH52mixzk4^@+u#E%%ly${l;8rQ5in8#R#HaDlRekq5 zCACP)1drag*JviiI+88E{nFXe-N<}J&2)4UxAUB@sOF?iy)abj&zRcE2koH`$sLZ< zOYB@=zkWh8vNm*Zv^UbT`gUcbZ;r&m!9l`A^6iR`k5S1<-_gz1h*8o?&%_9cQP{@P z#$L%*&%lUL#K_sqz(`SC2#Hb6%+S%4gqxKWiBZDH%*51@gpHFKiBZVR(LvtGUf9OU z*2db%+L43{iBZhV($UDCQAAi-#K^$L(1`Jgk+lg>nVplJgG)f*Pe{H_e%%3~3IgC5 zfeb;w2j=TENEn2IcnA3oA__7RGU{CvR16|a40Lo13VZ@=B04Gt23jgw8fH#0US>8S zHX2%fB>^D`DOovLMqX7dl_#2FGP2U&8iBcsii&X$gB%l+T$+WJMf$({^YsG=9T8>% zHVYPp6m$z61{NLWYb%HtfbllW_s=(YzkOhC!NT2!M?k!TgbWm@ya&1k0}Fc#4)*qK zI5^;`C-58uhkhI5A+sR-eFZ%PQael*pU5;sGNICsSc*e?!GOhIE9WqwMJuH3uAtY>awgrm)Z>W98?Ee{JzW*uAejxS}uPG1;EDXRr zSac8=G!wRBtiK|h$4Bc3yRJqSMZ#U>eQkgh@gghIXww^J;K_bs&)7<}bZ4xYk-a*% zSQm_47@FX~qJ=MGVt&p{lkqH-TImE{8u7s<;Hi80v&{$s$KR{If=<7viiMKYlwv## z@sEta!lgqv4LohU46F1~AmG^VUNkWyNY9kW++$5m@aGjUA)3OQ7P;I|zZ4-p{G%me zQ$MJu)mPB&VD6TcVS{0-A}RAn3Ww@Vzwq%lR zoFW0{xile{M~{9sq$Z)gtlrGzOmf?Z#_bp(i?!E(HVT$5%kbb75h}*FX4WqOXF!f- zuBxl~v(nGrvqg>gq7fUi#mK-ck!@nJhXT$mjIiXEB;mQ+qVX@GLkL$Fz_Hl*H1Qvu z(_mI7jJOMJWt{m&Q3LaRtpS{q?i?dPa3ChCzwJWYgxZ!oSXtJ-nNlANGoBW}P3irl zr2Ae-a;Mn5!57nVJd^Evkb$D6>kCZJbhPth;FwR7&|6aqWz-=q;i2H92gdw)h?OTn z;I;92=rxkS51`~TDbiA&IXA!Qhj&d^f573xq_HZq0Xl65$F0fs-g*C{;0@@}Ex3WP zhm0z<#f>Qm+k)Is`UELPKAqGdI!sQNXL-n&#irs1mlb|=8oY?NbD!yG;Ju+kcXhlJ`4Ut-|_eB3K~K zZ((-KWBKzvlu8-!p$XyFcg>2_u_Z#Bs!NIY9(@J1Cq5A_I9LI0IQGQ`Kf`RlXGM&3h;c z>`B4HgYGse`r_R?=(&AQJxUfdOAIbzd*0o5!fT{rm06b$ED_D+T10*-s<79PI>?As zrDOJG-r_f(%VK=Jy=%t_?NS^T9SP)nEy|F3*dYfPm`F${6FQk?7&4W^kirt3mp4J8 z<%MCK7#>^u;oH~S6lcstmrVml{f-4zMD+$(nSCr-t%A!WdT(6&=WP!Aq=zBp1wtix(9)WEs;{yOF6_nxJ}`Ov?6 z*a5`$c~)n|P3fV`Od9dlu-w1hAuMNdY_OWF&<-+x1e}j(`EO z`Fl$o1Hdc}aIB(f9m+1nqBder!C+#SVijLAQk>1QU9E}~FegT`OgM=o0oHmDE)!#5 zMb6UsUZr8q*g&t^PRk^$frw}H(&RTSJ_EwgXceWCt44m%lO-i{CN(`JKuKak*l!b< z-f|x4fH};RKl|YG0lBM7{}xnabow_9u@x~ilaAHOdfqRS&0YMc|b) zSD+bBU(rM1Z=hBg?fkUcx##oqk76UK&R8x$vh~A$@Fg^LN$eA_Dwza1y#}|ZHch%b zbV~5A3B{c6GF@*R7m;`!%Lgi~-Mlh*o2@1N06(zmkRoLVcj*P4RV5~fwI2U5F8u`G zXdHIgOj_9EISn{W&B6Q+$DJrJSLUK&hVliNf{#DkGW_r@w_Q54DH{jlQyR}$-%MP$~06`TSO&jv0jgyc_bs zW|azc174RQBUfg{jzzk%{VR{h?a37y;H9M{IPxWJC_j-?kyuS?!q#@Uhq2_y)y^CY zI63gqlbvHcBIk+r?q+(6mV*WPTd=|*9xzxlr4mlW$1kLvU>Qiggti833J#Q_miH2}g z4RzVR$u@>i%nE2Hf3lk=Fiw3PWH$7CIcqW^iB(UCsSB*gGg9?a+;5;~sw7pIKef)nOSwu2=%Ia)RaK6R4^6ClybJUwTKT#N3@L#g4 zjLCU|e)hne<^2biNi#)FGHq>%sH7bg0#vWSO8t2@NxF1wdR+Z`DVfeikP=|E>G37x z3K@K*{cvP~-*M9cNVThTovay>`WSn?p^*Xm7}#NT@o3bA64@6}yE?OPANnoddrS0c zTQy6PGNgmP0u825=tZOGQ0m@948fTj>GZ!>Sm( zgg)LNf7o22fOoE;S9zpgLFR5>K_^nL)rD|5Pn=50#y7#2`~n;B4Ki`g5we_dw~MhP zsEUzF^6a8jODpm6oiGz%#_20QN2rs^p|371Yq`xhe*D4>_*!c$!iII%J|oM<0n$O5 z7AUw~b}^?N?+%NIw>)ev;DJ{eAK;v32ldH?CC5nS-uieCoe53*rZZYGa-`w{#~2Xb z)0s}@QI6qGYcL636B70Q4(}@vH<@3`3qMf{!f-YjMyOyY^Z>GTta&NeHyY7l;6U2C zK%~O_0s(ZY>xwX3WClJ!u{j0Qt4ZNYy^)1chL!pmDA2~^-A(yq zDK4wtiJp_&vlYk*WcXpc_gWQQeTg}r*ew@+)W&V^>?MFlfTJnB1_IZJ(iN6Xu8)rw zAv!W+#!|E_t=>-8N{x#^$RUmB8QOGyuTs#zey8@KGTSR}Dfr`4*onvv_N?HJiec+* z(&{N)H_`+x-_~1vZv&H@`sL)htM+B0)S?zhlSyp_os92JhZuUgUEfvHRvp<|bX$=& z?E>K~t1Npz-u(FSi(BqI3%%&{tvd!rR^TqG)akC)%MqApCrXx zrtkN#7g*8wMZqt0JCecOGeem^T$sV>lWD8+sWD74A=Yv@%E_S3HPr2HA@Pe4HB5e> z!d-o^cn1SF+RajeiCdpV3=`#;o4p8*77o3&$(bDq4D$qWCnsziK2cGK@og9R9*8J& zU!L-90cO;k& zC*|U#-6e7-;g3GEB=NR$q$b(Ite=zc;LcKn8ivLUeW2;KM?8j4aN(JnmgX&BY4s1% zGF%!?$bQqp>trGZ(wuE?0FJUx$vjaH${e%lJNgrTogW z&pFg+d7JSh@G;0b@L|xF$7r9YX^QM+F3vXYfddJVF{FNT)bY5GP+)c?Uee46tDtq0 zblMH04Oy2Zc5bK`cb5?JdcOPP5F<7onXvV|c}Qljx*w|co8_fdSho+n!2VtkTl#2G zujX3pBh)pRXOCI1s@76l4s(jr3_EwKV^+h%`<)rcL@o)0*W4I>x#wSxGNmHJX z7k}aeH8_6cvxQl9Vtbb{OKLp3Bu-@QTq^T%>>GEV^2qibb(BytTYL!hHpy$;&o9oYrPLbbJ1iiEr(7@#oam z7OI`p;k0m(`l^ZmL{&T#BSVb`E}_FRS&}H2JA}+N=wXN-Fk$LK%1G;&4E17+xZ&JK z)s*HYnYWsDwjE&x*If^JGxW!(o}WsHQ+BsX48pyr{}ga3HA^5i^1?c zAdbMcbpdh}`3M^S)QTGD6PAdQ2Ohel3g&58bR&%No z8CfoSZLlR$^8(62OTa7!ZS*t(7gQ`gT(KfTi0^D%V+lhXxdv4bZ#^+1Z(5yDN`r9= zI`SSv-9;Tsu(?(E>eJ$RthzH|!S(%&G zMRrJ9PkdfbD%3|N8RQkz6nphZH%y&`qfjIDAy-WIFDl4i6;#6+*(F`i3I!!nCe zpJiZzdKH4b9<_m3G4)e}nIEIH;pQxc$p8EBI}zlcv86Lrt%wx5!i7Z6;}xjq^8|~c zOJ|2BXOMtU5vt6XaJI?-b7*=9b}Pq+1ad)@FF9VftA5Ulkt0Em;5ESn5|xQ2*9sW- zL7xd&Cv;3qy&Z|7mY$`v41GxAYtO&``2I&|F62F6uJO`iiH~*mIDy0DeONtXK{5|r z%nD(*&ZCpxG&N0=7M_bky~Mv~admK>%StnvMJJ zMMia7Ue|FOBv9C)8+>>G{+187JmcqOexhn-^+xCB%8dBw$VIq$Q&lYm4vtCf1RgbQ zIvAMC8UUPDApdTE_>Jq7NqI>w*dnq09#UWRcKn^Um+e9A3)GKZtq`A|6Ms*YJM{B% zj0~o0n0k%;3UfbIb@MnzGd8?>xx&I z^qTc$8WW@%uRA<9cTH@rIqRMN{7>%QV6-&8HLonE0ZG^k_h7H7a*W?GreH&arv2#? z61_&&)7a7mVfXPkMa_qgtd=RXWd)QvZ&hYVj>b`kKJ89&3k&=y1UKZN&_Y}2Vh%C! zg#R*NRldeh>D1oc@Itk%vQd09AW=qeN>YhW>nTqs$k0hwe|>=o|Nd$)F4It21rLJnWSj==b9Avy|yqkOUw! z$nP#cLqb7Re@`$dAX5T3I;S6iDMj!wc&&L8dPcnT_|8wUydj}L>g`O(fd9nx9=JXH zDExd6$h!C*;cW2MPdT~4ZO5i{;==_eU@sZke#SK!^f^P*t`C&$U)TZVB>gYl|CO2K zf5+;7c|F70>BllPBi1x$xO9|Lj3-Rp$$uBb`nM)0k7E?0{`0vyh`^d}`2yzJJOhWy zP-G34wiEKVx?LGp+XQTC2lxXzds&+6R+GpjC?ZRJ3ZYF3*A`}at1+ic0SrmWRg8Z- z5}hci@sYCPKci-s@C5vA2}jz_CFZ9y5>0c;OUGF^jSb2DzeV7eGQ>ZP!&|t&HGS)l zANEN6hv_qg`NgsM<%iBslQeWwzk=j$_+&Tzv$&sD3G&0RdH;V1{4K`PjTD3)Y;bZ_cm`2Y%1w4hj;M$?KiS@Vzk5Ro)b+&+l3Z$Z=e#YdCUD9Z8sOyPT&K zlDF(>G5Db)ksjC%@j0m`HtLlJVQ6fqjVWi>AnabgWKLH;?OeIPnOtWf56PED zB@?MI)^V=Ti!cZ}6f=6hOFzqsh|;IabjH_igXg56(um)M@<>aiiu>|fU{_+ih!Q;x zt==|d?GbbIMewxd>r2V1$fwjFI>kHqOhW85s@zK(Mzg|B6i9dEC7NS!m-gTiq@%1>a4Vv2y6T@JhYW~1_Q(H3#Vg2us2EZP_Di`f=nwK7v!A09Id^MZT(-*^rRf*&dJ$GBbtvl@T7iIjW!_mQ zL8kteNKe7HBL`Su&DD|ZebK$rScldf1_>ZXA8o}@SF{?x$%n?5UZp>=iz2Qn1of8%?i97)kPl7naFL%8b^LPP8B;i z-=}X|`{ubv7)-z2z#ufH+RP&NaobX88p-pvj-GJ$LzS6r`X(h``r0gt^rkr$XfrQw zBs!AIErrYd&0Tf1$Lkk!$@V$Kro1}WZ`I$Od8rJxS=qS)laRHZk@=~~0b`;&Zo68? zGd+c8DZ{k7UIW%*^QSmTsd-Q*DcY2M+8Bp9UPqqBN>z>UH#Qg{u|3`%rCh`@IshV^ zAJbx~)%?(>kJd;g46FF1IMzn%uVcuVO=6^p8Wbfn${*s7re@tK=xn*xf+VDCKNS&j z7USBCR@iwn*K};aD(5UT#CcU30dvHMu8D6!EEAd)ne-LZqlU0Yuv+bWsR|_Q$=fdv z+-Gy)P}?SB?vWlF-+`S5W0mo9D0a{3peBT8y7oBeK4O4 zbHLAu<6p@!(adIe$X-3Ic+W(pN{(53T_Qvf>3rRuSfk&vV#PLLT)ypR6 z=Z>wIggdt|FRGSON3iS%a?K!-1yx06_u!*61Mx_tJ@&~(A$w52hhs@LO`dN?Idt@jiHL9-q-@7OrvRfSC+bk9l zIapL(%zBNX#~?rP!);eSJgr^d@}xQ9$xgMqFMa#CqBJAu)3A|lvFxoVG_A;vMu(|@ zn0(6MpodinpG#tgmLzd^;TXnsqNALz=e=?W=43c7G_o5~<~~wI2;#n|m&TQtsoIKB zs$b<=@r?KSG0_)`hIT85$=VZIU>hy}r5_Bol^YZk^lyiS5J^O%XCB7Fu{zunOc`XWfb;^TM#3HG1=%U`Q%_uLR&iJa?l7}G@y)L?9 zcBVjM4%Ca6FtB(@MPlt=+Ssmnt?6`fg}5>H@l9WmnT2VbRi4HxHLF+5$8R%wtoeJYm@k6y{VP z1{KdY_jV230SoK8Ji~i`jc3i<3@(qPjSo({YA}!@F5PK~)ICSGFXXhO+jazwI73W{+!7boTaqS@L z(9e1u0NCl(c?&4@>1(^1AGi+kIkR?tOnn~qVM=P4H7>~iyjJgeW{cp^$9Da+%HGB> zOjDZMxFIBiRfe_%5~r($s$C1W1{*9~t$$$QETlx()>m22)`v+;zVSdm$Ls@m+t45- zMrf_gc~8#Dy3YhVj1Qa=I6YX$bxI766HM5 zeV>oW1LQjs^A>>^=qJY+urU4Z3iT#Wc z96ELj>2`JOgRb3M2(G%d7+3`MJixiL&rX@pxnY5RYjCCIb8o0d^fq+`2WvHVyWLc2 zaRWav^VWITUym#W*JKe{78a5Z$AwtaiHAppkYX-&op|M(3B}qTFc%VC)t(Uru; ze2n7SF${E_r#0wRp=!&>@)7rOiyD`tPe+Gj_l`RXUfISjQ1CzEt@hG8DwtL$d? zSEr^WaQUJKh&s5)u)?P99;uFqn%&NXSX41}tOStjWGVbX<{KjAaiKc5xx&bJe#m{Mh5U^pWiC zYI)k;Nfy=A(^cg|qw?69&I$Xz#>u?9mEOt(#4)az5%~KX7mj5unC|`sJ+}3Wq6yLx zK#Xm^Ff=(Sk#V+(DAy7B3X|&F*$O<-_Xwp?_QT!Vpv?_&N)`q2q(&*~tG0=+zmOUs zaXqd#Y#$VS=!y1Z44Ia8bC;&iNo)g?f=J-e90N1V91qs3E#+Mzu(xq3J z2BY4l(uL}ePm%d&8)&U6q;^?A{&leZf)`8NOKdA+M{6}m_)wPj z`}B1_&1^i4X+4Qudk#+_Nf{+=$@3D%&??87Gb~@lhu@r*pG+ZQz=ThA5ylCwOxv)a zHq-!21BdLZhBw1JK%k#E=Eu|hC_Ms5MDqAQ{5Rfk*|sKsnZ8{7WimD991me+e`z#i zJVtetUiDxso$Kz{ovrz`BX%szqz7RuViTingF6gX^a)ft%xiJ?nu{}G!eqt7xccGk z&PGq=5gD~H^$2WW2T$|(c=dJtiz&r4bZGLIV&oxtcozo4S??e2uy9W?D+x*pI-q7H zC5LQQGfhjAY~)K^j7b=ahC6t6P8?ySIU%H4(Fyy0d3>>0VwH*FPhdb&K)pQ~e9Sbo z8d6ldyi}azfz9YOQfge4fU4&FApv$W<>(6`nEPd&*ey!WTle1@d6w{%vZ6Dmmh8TJzQyHC;=#Z8FjhyhO9S2L+)MPpD6@ zYeZcYosCsNA&7?jXv4YcX5xzF!3V0+3S5U|!N{&b%491j8j@6m?oKCtIulWN*nJGR zk5-277l)rrGumzkIL zhXMvt?L&pfydn{TYwQ(s2EsV8#ArlZ7sP9tDt%w>%h4YMDjE)$aA zBUVx5p{C8u)C+tCt!oYP_k?(*kTV2mpWN6{*62MJ&Ny4YmFKpe;g#YGE6$~M`1!=q zu`=^>+mTNNv_Ft|W)q~Oep&kpdR3Gw*+%(dF$$yV~^53JOx94gx0fm_K;RJkkyQqin?)11Zt`(wb&kk7!Llf^Fw z#Xx8p#w!G1^Nd4Xv^$4&UYn)=BSexAzfo1g6P9L4Z=8^?AXGP^x#O%AAROsHug)1N z30;{XIVUDS|Kgb+!ZTN!nG@-xdn(aD zTdcmc0nRPj{cQ_@aI@nfeIqi5^mrXwJ;fH*3c5AwU4Yv$rqcoojhH*e?)$1qK9n8Db8@@#Zd*-)hY9Xl8c{&z4N4NJ3|w8c0jqM~_*}ro zY*Z3U`n>E5Khh3yd^Tk&T`ynr0h&y`_)_BR?`marhk6!HRdyCxi6BS&wi<+ayCbDq zady`NOE>XNdjvqlFGn{u@Zlj26gCygkPz9uQOAEZ^|yxR>Nz zuz2Uwl~{c9ybDV^dPReVM_8y?m992zUkox zvnYSaE5empUYF|$r7^i>z~_2EfhQZ9(R6j+k}m@B|tS%jTTxOT$cJ4Z|NNfsbYm&v}US%U|`?FD?Ns5*uJ8+c&DL>G^;6y zK9SAhrBx3XzCZ`r3<4DdNeE>xFb9ZhJ~Xz~IxO-))$wkpPj{(8cYy&)3&ILq3L3!! z$2zryf)2=F=RbweOHB^kCQMvi(;7GN6p1{+fZN-*#z8pcLbmxnk@gO z$+Do1)INdYR2%(O7I_ka`gp&}f$|c5S7*EH{6XHw4&kRm8ysaa%b72FR@p>3ID}!M zBg5bk*K){GMCISKlvUPEx4GvbPh}N#n!4bjJ9HD8g2=bC#?9wAbyO2vokCGk#yK=#XGPeS*ZlYt=^bM?t}c;VKkrc<<8hBG zN!>kWvxU&^otIZtMsP7`-kBi;-VPs+fC)U>zN*o*!_t#QS?BBCw>r<)wcz0@Y4b@e#0^KGuv?ABJPXPJW&V+ z{rmHO2{qQ_=Axl>K(48@N=(lAzb?Ozdu$&DdU6e9?_0!?pV0PQ;8Z70teQ@X!F!Z) zP@1ymz}`S2U@uf%(V{^S3esw4MD=4_Y>2BW) zb}*2oHiRgF-$MC~^J5?q{;Nxl`O697v-?zEL0IB98qcb#_uWR;>@I_uK**92mf>ZR zVR!t8)-ierm=h?RLJ?FAyulCpfWW8GK=iKEQb5CVo+;v4{H~w;ub^hhOyD(d64=6p ze+4X%->j%qj@6^{2{=zZR|CAIC4kTJ!R5f==tf{Ofe#C0UquayAwvcA>bTyUUl6|{ zPn5g54!>MzYJSC3`=9Jl!5D`gVho(ca9;vaQ~`&^7ISZsR7K}Zk}dW8lK8X*;!nI6 zWd=Sx0q+87G?3_;SKe&_8TYgxpP!^&sSy7Hb-`EA9ZG-;U<2rx&0fY0s72$oDsQqW z<0he0APeNfkhLwy{9Py}A6S`k{ODLe-TrKs zz;g54TQCzkQv!s80HTM!1fL~>-uw=uv|q}BtLeaNnW?9Xa!^Wd%HJBjT*!@b;oCiE z=j&A0XMmA^`S#8~nr7mgWiu}ATcGZMtK6NPqYn}Q5&!%)v)qri3#Q_yScH5#5IrKSg;guGP*E1{wNnW4{>XU+; z(c$5v`pit4^Q6~QX$YHgV`Jl_GC8N{zhE?llofs7UJCxC%gbXRgtM!$P#1xPZ8KC@-- zKk0}^aJiWFel({vb`e<*H*6S&`ml#rGJ22zn!_H)x?^TE36K?00LazBzYE^2{5IwQ zx0hHJP!p%Ar$EYueYW%C>9#26FfI`VHZp^}85f^{Tw$JAj%4(V<7aNCoj|k$c$p~71TQP-}GSi6*N7;2v3HNA7dt{fUPnH4#HT1 z-Dmq$c{xDWh4DDQ%FJGvT3?cjj2rVE!_uJKKO$YgvZ+SxXxVD#nXVQxy=SXXOMW1vQ+)BI@3SH42f13wFco_siFSWEbvm*jFtl#@EEwC$^5wooO|eKw&P*!z#=IWt1OiVZd7K68FCIP&!~IZxDTtc&lDE{Sb1`OR zhJWan>T08WgnAZw4z|&=IKh-oD4QYPa(pUX$Cq!6Y7fa?c8lL{SH1`T6{MA7cjkp< zylC;sQCn_%5OE*&+>(j&ZeWRV$C~WH4$=C`djp8;&?@I?sMVPQ3Cnz!1Dr#lYT<&m z;4=CM|HhSbo%EM{&^*!{osE4iwh-cgBCAhiN2T>_a!6Ed_j%~`l6d_59Q(Us)1fbW zVo|)6qfzhS#pdYJ_8KS0$Y>hDaSmZvtJd9fwM6bq<%5ajDuGq;C>sHMLTy(U8J`_Y{Hg;gVEgmfd9}~L>=NE- zKlR~5jij6()%jA0MhagH&udGR&=2{mV<366_M?}1))ea-sl<$P&iMqXwsWFs$!Df~ z7!h0rpN0|{d-Y(wU!OAXwR?N_}OGU+QOl)XmpE4@gzO>MI z<~|4gagSN1vSTbp2dFax?plyNON-q8R74DDq%fI)zLNTEfhvw-nVla8A(mexAdR zIrFm}2FULsbYG?g8n-n1(b%KfMb?j0%01d~WK)sRnBr$y9||sOZDryjSmwOQM_O# zyqqmXn0`3ptthzu_Lu|}UlQjtZiL`sRoV>u^5YYuVG|^~CUfEpCO9vVF^^XT3GdRw z@G?Rbw_5EpcZgKC2$)W<`Dq~cAe}UFr@&mx;CPJhiiM`>lE2tcXXd47+|`L6^b(6t zXJcYr!&aK9!AGo!u+H<9_lJm_9s-90Lr-XZiLKW&3*#-rn4TW?3?>zTuHLXAiEn!3 zc7i)%-K1VbKMPcZSGU8oOWzhSfBs1+)uuhQz(Ub1L?qTHF26b|Lwz#(8lSK$6k_xF!qsCMnib+LKuxYy zG(~;07!YG|z@B7X#Q`>{U{u*+rk*tX#1L{grPK9I-#ap-Uz?Z3%h$T`wq6~KXk0{&bAED^*4aY_=(N4^iU7~ zNb#RHh(ARF({5%E3cv=MA7fg~&;^5U-pUlb+hF|}qnp=>vdRJ(mEI~dRGLjs({w{@ z2J*C~+TJjVvbn%N>OLOdt;vr9N%!RrMlRFKt>4WCvv$z5lPhiwMS$RlzJi{pCi2w5 z3*o67qR$nx)R&V^J!=;7<(-G24(6koDq6=CIROk(Q(u@&?jkb?&m`Bb%;&fM2J zP;lxOa15q7K;kPPc=c6ovFvB6U%Vk8pCk~Vq@ro(9kfsTu9@6|uqMI$%Pp0aV4)We z`=#~|J)kjx4)NIm!&uX}NZa9yR7t5>E?fBF`TFu94g49D^`myry2V;G=lOkRj8pQh+ z(9025IJBHBZ53tt{98VQFtTA7IXrosHjH3-e|+BZ;Imj;w%x+iOL{xn)4=gY7mZ=7 z$^vlNtPLVAPZb7RXSn5y&906X@G%dL!{G+H4@nTr6GNOIByY)JArWP6~C-)MNlD0=D{mCol0{@f)b`+8?LXY4`_E zL*JcAT=5=4p^VASoICidI>HA>`4fpJ-gVl`*otlVeaN}$pN(ePBnS(5{|O|Q8jp^L zmXoBg+I}2{ROF6SGitzl#SK}>_!Y2aK*-8N%F9R1tQ@%qZl89 z9fv+uXNI=;Sbcj*Zt@AE|EPhG^m_0=5#MY0VP`)}@D`B7>~|7#5{W8i_gc;5|7}C2NL3POL*5OTnTrZpicDpf!pwI!VfKkfNs8>v~Fr!PHuC^@ORD+v_@eZ4I% z9d=ge)2sC8gDq?17jgF{QR!%#_+e&+a2lqqD^3D9x@enM&b##YO{wOiHHFwv)JX$$ zVYrrk2EDSAMX9Lh+oLqGrkeYf!^A4Q8+73eaJzj5VW%?uq|1F5iZSNETuFhGEPke1 z)E%(5iWNjBw>U&3(W6Aw#Bh!DBE%cjpuHsr&r?e^-88E$58|wMwG4`$rbl2hu)Xnk zb+)rY-e8EO{j#0s`Lv7EXsk`n_cKu)%WGvwRt1j#kL_hdR zGJJa;0{1bS8?I-CSyX$p$i4(iSeCj89zqeVPcqI$<%O#0aI(pL$lD5EMp}ny=I$jM zvfhteOI-b2>unZAFLF7uKG5*kgj4O4_*^Jn2Sd(xiLWofS1J|**e2b*%;`)C1nfzI z+Uh}892i)B2E2_0suCG-HVF^jx1-bt*oq7BZ$L`r0M`-lf1AXCMcRxlCBP?h1>)-s z0bW_&;oB7>4{w=CeZ}&};CSvW|W#)B}(Uukdfnwv#!4f4L7t(aiuB zeU&A%ci0jsE}s*XLFR!6NCNW+g{i0OfW}L|1j6AQ&VetFAZPgkZQ!fZaOgELlqS2U z=6X@5`r{_m^#x=vV|&jXa7MgRuV@(m0l3_<9in~4zaUaLIpYTwOPl1NSUA6cqi{kE z_{j~B(=UJ*r>x%dg)vtlh2mXcF)wTX6PvQag=6i5-g?gh+{r6G;S zmI{9{lq{HyBbTmlrwQ?@1=O&`Gsu7gu%!R)OoePbs*%wMwS%f6&i?eIa_#CAeA*3g zg?MTr^>79dMp`JASTO%BN5>Psdzbs3b|uSu7j zRV2HlI-CBY#$MBc_@Bbm3<#6a%UR`y{X)AXZnWqlGD5F}OwN)=aJ6^?%G|h{DO^_; zawe$l&kuHhSHz!P{U9ySn1+s(-({~HADq7HW;=fy?>4iMm?jF$-HL7Q)AmPy@Wt%6 zl`Y(0ZwV(EI7>xFZg8z8(``gbEv+8-bgoacd2l%s^VKKn{3Y$kzy^(XQ}|00BUg}d;$eCA#6Cuu0uVE}xvt~NdfeGB%;wRykclres8xj$V8-nnw*<|vY(TB$^7bgjk*uE`mb(WT>Wm1|dK z#{U{M0IV68PYF0Q`x4C<6lQT{bK8kX}-eof~R8bVe_X~IbPO9Hi zTA~4Qg-Sk*+55$Nhg8cozo``K#hGs0sM;?%Nlu`p=&f&D7654WbNJ%?9KPR(R#E@5 z3gzH~qvB(#D!Z%ovS;miwZb(sI1lj=s0aug&c4x&`}e><(+%+sp}l4A+RhCm0PjwW zvnv&~EGWUlCBG#uc6bY+i);CoI6wEgG=H<^=T2Pees$`HUN!6D<$-zt z%d&pQvM!!iE{QB-Zd0;|HWxm(zO89Xmh+Z-*?IR{1BX4e1;1;+vzR`u ztb;r>4Y!(TvhS_~Qaiaxb5h9XlYU6#pDZ8wdzP1tAHZ|XiEHgBGKUutTfRV*hE2N7 zTtcC-|4ziy-0)(sgqfV^5S7|dnTMmKC&xHJYbcn$A&3K2tN_Hi0JSy$p4#0w zk-FBF_fvvp;q3+pyx5lOUM^1-N_G)DevTY9$s+0}^dDeYO}(LjepoYL79xRt)Rs%b zg#_HLwsK)oNjGBof=^G!4#vw8HhQxUsO*=b-N$%?xi?UtWx$Wv>ikR>Ec=+7z##2P zRGje0vj5JKEuwQ0fw!34lfyr0UHThZ8?-$SpfE$l^_CE&Dxs<-G#i|2|L_vu;#Lc< z1i}6J9KnCayysGjSCS%-L^lH-EEtmLRFQ339T^hEV~|Grr+`ZTJ;|~}iMk2>rlf0K z{(b(JzPdSp|E15L@{xgB%Z5Q+W4n(5i%~URJpchz0^rV4< zYiLiU@+UBs7qP)oauvX)Q$Ee3chXZWwOv~)lhvZimrv#o+nS^VMmVqbZHB`wP#69u z+_$WPH;a{g%9q!#NMGp5VsFkq9H&nvB~>WK>fdhfkoabs6*7Q*{pe5Nw2m&rqYTBL z^b)ClQNC*85w1ugx;j*~qKEd9<5$~6G(9ok&j6!#W7~qK5!}Sr}f`a=m{( zoJG;4o14lMFlQq3jweYs5B>APhmP;|Z6tIWa}4ahl4q)nIAC{YJZ5A<@4zXT`Vp5b|qWiyDbK-sTj;e_9W?6!v9c=v>O}E}cVl#yf zyfu=qAgNbdq7BW%w+?9GAKG$vSY9fpS_NJ%9cF9;n~t7)IX>fW?hUhmA@@JT$7o%Y zI7NL-$hxhHVzrF8WQPv|(PY4Wp#k5-f{|#!mvYq*ZIR9~pur=1Dm z$??*7&L7DBU+leQR9xG(HCjl3pn(8^;1=9H6rKVK2<{Tx9fE~GA%P^g6WrZ`Ymnd` z+$EuKNrDEeyp_H8*(Y1>d-uETobR>U?)|Y^t*Tl&*PLUG(MRuNQvGsmIbRQnO(


    T;s+y!cdxf?lGfwZ%3vq91-prw}yx>Nu2LimAVzEWM^S&+FUz?!SC-}|9N1L~( zWA~3+EO}s*Qt^fk2{Vya zyd5Fc>+!S`J;Qrjl-X~9$ue3oM~0DcXdM-I{_M2ykF9);#T{l*W6PG=bW5lBi7R&` z3~8YtgE$qW__b1`-i-04>K z8yZs$25D>+co+Nq;i*dvhtJi|s4{i?zZO%m&QwqXDLOjepGF6bC7wfI2y%>rTH}07&_Q4R zqeQk6I-Vd(%~$iI5){V=J8FM^5Ij`-XBb2lDzabgFJ6M2CD-%i&LltL*Q6nL14Z;< zz3IPNgI_x%#@+@hiOx=KRg6C*+$(& zrjkE(&5I-#*XkZ3qd&A|q{Wi3u4!oH&*zinIV3Nuf@uHT8FG_7akX@EO8;;p!!0ja z*;F+VKezR!VOPRm&+W$Xad3!G6ZW%_5KeO5un1*o|9K>k4>kVxy+$1%YyK6;{YV=g zcyCO1B%``KUuBM8Y%-`8@G5`sTYV3q+Fm&xuCj$>ax@=&}MmH&S zrwn-ZL`^ts#sKhSr%Um={4tUFKBS%|ablEcY0qB?ghHQbEXtGWlE_ci#DrZRzDZJ9a+-=IYxJ z+ypcpl)tZukqz0sWX9=M6C!1ayxmtx#4k|Bz`Cz>1YA!hb2|`wek%HDy<~*2>>jH( zFpXgRY6|SD#_wyImZ5C_1O9k*Aj`8j0H{B`-%4DrrTHooUX?P@K6(;vc7Li2 zg;@(!OhSFaPf#NkXF1%H+!_c4$2HmD5Y@2~zMImAeUW-|tpE#_ym^UNZUY5aK%}s} zaM;Ms3HBhO>*Y+;zL2k-A)S>(XGj0oGC&ZT)DuRRHM-gYX55w_HJWq29RIQBllo>| zgn9rqDK%y8l*S2C!jB)RfPbfs?1)tjz^FbCclde74 zGjg#yuwEI#j;yL?+yKT{Z9of(@aM>tI@6gQj$AwvXAJZ$sdqBZPQ*iO=0i8|fD5wz zab*?k`wm6?(b~saG|WO|&Pw7BYve4ofuq&_IIOm#lUaSn!aZ$v=Z!o5To|OAWh!XD z|EpK~zhD0QwgLZW6rt8cKOR%?%Qe=#GC*SX?JL0QuXubNVWWaB4uI}`$2aIlIT63{ zUWmq)V!%Hs{Oni1gNlqU5AGpC>LS*Fz0Z-H1=a^`C$J5#c; z3k-eOEVFou7VH&!hOa2x|1OxiIbiyw`?ai-3aivUto!{cq6vCEks*h|=+Ei}?ODcG zW3lN;I}ZY8|A~MJiCOevV88({_wHCDaz#g&+m`p7=v8zq6ZE_UmQUkCpn>{8gH*{MxP(lGf7IZFbzy{RHO)%uUV^L)HFk^wqwqg zxX&Yqwa*zS_t{K^3&Z*Jp{BYj&Tflh3AyYc>3pw%qjVDb2WwOcZBaI=u&ClhH(p6Z zu8skbOsckL;gecJ+~KjuT5B>e+}l$M5fS2s=i#phyhYmCIiX5m&7P09 zw@mqjSg^^UWvKUj1fecs@L~ibr0PmqRdu`QpoGpc2kFBlS!t&O#jODAa4QG#Q~;%@ z;NsU-*ggi>I~idY6C}ILZT7I^ao9ufZ(D)>FZ_b~st;)@Q^FdlFJJQBb%4EJA~3H7 z!PI6}7(!ykNC9?p+lD&_Dh{XPym5)Tcqf+3c>MxkZ&Yx92MHEVkZc>BGb6Uk4rJ96 z7c6v4jy3L-JtXMMLN9=I`%Lw&4^$H%H3j07?ka=)+_gTGcZcpO$l`iaM@&!_QZ6C< zrqke)stT?j9p)~wLdjaRLiwc(0%Z$utTkEVQp1cccW!VD%Q3j(AU+Z5Dn7Zi3q$nw zMP-di)}L1^CrTv?j2PhZGkrsmN4nvuvo>1fE@_B_{KCDN<`;rI-LW}~99t_OdbdgR zxv5K~V%&1aV~JSW+<00Eq*0`OM-SG)whQ1>aM?vHTI2^>lmSl8;1~rRDUcg`UW@I1x637 zTML$%>mtxh(;$Bf?KBh zzk|@G58^3?A1lZqhq7QT_S>84GSZ_thk^#^PqJ=r=Y)co{;*`#H(g09wkeK#E2;B5 zK)I?c`faO%A?i@Zm})6zvnbn;_eUaELWeon#nk=S!h4~hDu|?mH_lW|A3lA@6?sfR zN`^lsOa4kWQT}vYYv{(I0xAm{8k+ZJdcmq}(E>2v&u+LJQU`iasK>;bZcZrkulMwi zyZ*0zBHj89y7pRv%|1ZL)RqG2I|SHKd@l}0r>BARwg! zh^sYeE=1BrfXyO>c!!Lq;iHD*;lwR>#FR!nN}ah^pLl{=ME7!p9xJc>)PtrJ9gak9 z7SLL+Ygu7-`NPM$b9b8?RUtf3HNgmqw-W->B)iVaM`+P~zxN>s{~R^yRxCOwqg@{4 z{-a_Y5g~8c7!MQm`ztchQKa?bLTMJo)OClU#tN56$<7-HNG$Yqp=OgDhR8B$=YhWj zBB#pT0g`yA^TaZQ3F$d?r!(Q*(KRnNAwr~fh{?9#@rWBk?Tc2M))1#w3~Gl_C6^Sx zN^e-x$EqkH=dwW!o^WJ2HkS;Si8zn@+5$GD(%4+6l-=6m1xB`9L-6I^=Q+Fh=>bCY z*yh||O7DU5ijWtRXk}_f%`$GaF>(7Ys=za$)~CvEN4%)x0V0B{gXq4*{dYElYC5-4|P#p()$$(z$!AFzt-&D9DEE@BD91PZ(B zirn$uEMAZ`uFQct2wW9gYNdI zd%4{?9HnJ;PwI`(GP%P+tvaNrHLNK~eF0Zl%DXga@To#r3mG3v$_sh+DFWw<1@BAp zSrz*OIb(-xnpGd1pu|%##Hre2_O0pMca2Rv5L`vB3uAkFkdn`Y+V&Zlq4sIf1&tjJ z(Z|}+Z^pD>>{yAt(-D(xyS%P5);EWl2S+qKEW&1VW{h52lep^ z$0Z=;sfo=c_Np$u!Kh-JS!Y7+ly%(4o!+S#o=E;^BBuF?LiRZ`5!pwFv4&wUMG|Rc zynqRJ_k)8|Vj+2;L|hk=jhE7=Tjk>Wi3qn5IApVNVeGA=$@XJyp(Z71DAa-SeBYD! zYuASvHxK3Z-3;ReqV;=s90)zGF5g6R zioIi{UF@1*fU7U9l$8=@Sj!#6vb7hp&ZMNg>;80LM*Df+ZJ_jVv69*)I_(@OW;{y# zg2=HJ@#K}V&g7)3mh00m6U?T9ilO^PM3XfgM0;d>qB34ZO>q>WQ!rZJT|I58FOd2B}p$=m%nfL=|Ah@R7o2Xa@CgHXmawsVgPx7{@3UXESw{Y84R-Ba;O(-9F*$8wW zRsSbk-G1)sDifm;>nq(@d2G9ha|U_Mcfz zBnboz6?e~f(AJDQ1@${p0*G;wkYY?Nu0JfoVNZJ@MaRgY+Ri<~4?1Jq9noQh64R?( zXOAUf9Fx`Xg5II&p7Hxk6_NWXFIgjG`WP&T!tYmPQpRw;iR)dpRi!Z=ip0Y0qaSx5 z8)9z@re>>eVEdf$m6P=%vFGX8na50i#Jg6N#H(IDtjr4LPb4pTP43^;ao`|yq?_b( z+vPCC#oQZBIXm=rbTDa(Tr1(LWK0VJ-_q=-17<69q!7abynkD?ui#FCN1dK`1ReGw zZ!Rxqu5=f{pFA2~7f{eMVd3?ai<-P<@Ydqfr6NmX8Zs+TekNQ?#Zk!mH`k>0&RdIJ z5Bu{ zR)Mb<&OPKVz&n=^=>pTZx(=USYJrNK%(XC)t8fapH`4Lb2D3D|z(d(N)K?D_OywQP zqR7*H?b0X9>FQrfw!Ui2u&Ebu0#_)LoKy^yf3#67$F`Je!Ac-q+{u);u5G}&ALbR< z?M8yZ^>XT&N`84~p8`2#xh{=(VNIgcIqnvO!>z_OBY7+16m~_scx6#8?gNKfZOOOU z`F-u&_1>Ckwqax2@bqRk_H+rc3@7MXoy{r+zk0qZIH=6)IC3T9acMOX>A-$tFJ=g< z*rF+vn~?pOv#;|qK~gLz;8NR+5Q>TVn&{3`O%mi>1rc-n7xlr99=-YEZ$D=6Ok*ma z^@V0&C{r=hAN{#M`*;7j?}0ygPnxMevU-A4rvvRV~z zE1?tX!W$4bu+E2FN}_W@mL5u^Y1M%OdOl*1QHh)5u0rGpZs)45mAEsg z`W+JLNomSyT9V9NimFByl3T_RB#cyE-00gHaK8k{K82de`IH&j@O5kjU_v+o+YQZ$ zXLl#naqR?C4tI%eApVVud3ISxBgQ#6g< z?j-GXF~lLPAQldDxUbS;5$Hx^ewUZzd6V-lbns)Uzw*c!<4mQFjgtcxcXq>4Y$@2s z743_9g)CN@OOO4ya&mCEFy9Vhxv_gvVKbLuFMg03#Wk8s#<>e*e7G6w%dt^j@LJ;& zHJv}2L9I^^*H)xI2ObqC`2KQE!uww0^t=)SsR?~~ns8w(p|A~8brOvWSt-A!!gPV> zw{9OT_P<6S7TbD5lG$;K>I>Dgg`7-YEb`@yI52m=9$8dv39`-`Df!*bfWeQi>D`^w zQ(A0n+^r|X6E3&P&oFh zD_?0?oK6N^DAXORokI||BqyfH7+YoR_wuNsHz#&xMVpfzUj#o;7l|*mk--e5D|D#X z;1JcKfR3(yBmZ6VQ5|dDn5@walHD^heXrYGorD(6@e;fj>Wpt|8-0%#I$77rGPwGV z`O`kZYx!QpKPePGdv`9-C2AD~z|KU|(>f=MB<8xbH4U;)q}k*#u3fZ>r8q=h!1Jr* zV83{;ke|Z!p=+bh4afZ0Mn`DXmq7fmt)R!t2mLuz|54{GI=ua~SA6s25{*z50{XF7 z;y~6Lp?!=({4S;Qh*q=f^iojN!yH*eBfB!Bc0F0h1sswTPq zVUlqgQrDQ`qid=*hYY1v^BVKKd3dE6TtCv%1)eZq4ZI!ABqZ9V!h%vjpqg?9Z${4U zj}lSh5&QghtMVN?iW=jLnlQa*MB8AMEbUMN=x~%bZZ&EatkzX7?d-qU!#1jbLT+)J zq*uw);ZWqfF#Q30Nh#YPF<(@m@Zw-bx+Gw6Hgj95npq;8oR5@RUZ&FJ+$kh?Aw@OE zoj)(kvK8g`GBU#`I7+qVF?NtK#!w%1>)*!MDu?3clgD(?LD*DBV#C+4uUlFug~m2i zrSoL(_ZHa57fF2$ckoaFnf1y_epnBU-BQ-IH>ous`H;mBB_jJIy?^~{_8d{K(TaTY zQ#bt{YVTx1Y>o|VS#X+F=IEQ|TIZhR+$NUnmnk2`JX92L@t)ur3kj7TchuzM5+GN< z?JJ^=+R&t*powp^vE;5lqU&D>?|?`0W+k$LRnaa8|&3kslT%~{A^6`Lk3Y}pq!Biy;>nUI$G9aQB=h!w%B_y7MAwr z3B3Sv^UN!885onOi3Xk~MhZ^__Ufds-! zB7vV$Y&)>)R?ljq-qUu!a31Ck^fNN)=*X3rvez3{Bm54MZG`x~bYodV`v~1?TA#^{ z>Lzwg#DD45`yrU*EgQPTJkX8N$|{mEbAVT`7C$`lm8*!~)0mxXe6uS+CPBnZ=J5W2 z+8=l63~;~>!_)+)mIg5U>^L^x^w0_{4;>vM&P4IldwnL|eJ&0Z=()Mkmf^=}xD(t# z1Lr5ACBumrhlb`cR3pdI%;Xd;F8w>9oc07?0wG$JH(rk0wtI|{~6MdfznnwJP<%tJ}@AQFcIwJ%7i;ah1AqPzzwRW5~nd~MFL zavK*O&G-qu&&=`1Uv2SGi0TO^>gLB3gv%T*N_LE8p@<9S-v|1GpcL98oxBv;f!mb4 z0aHg3z*r~|XwEg6(e!3mk&-Dvp-S)NO>@4tSg5Y;=m0*8n6aUWK|)$zOry+=mNi+d zli+(3p9n3`EkmZQEuh7d^~=*GtKErI+I{ju~W zh#o&0|*g+bB)X0XQPk*iCe(oIddMsG4Q2OTFVE(NLF zpo!F)kFi-w4Yr>e(aPsU_9S6&+WIz=$2R6VfOSb@GUfRHyB2ByCyFAAx`eOx&m>r` z0{DJJ-;Jvi+)m#AZ^f;C@YnwltY7BzPj=KFaH(G;uXk2p{Zfm+!1)CYfSbj5ReEDf z1|ZURv0#62q{>l>we{Fj`7|4ykM9hiw&`=t^;K+~jOi7M-kfFHm)sk+RvBPv4q*w7 z{9w?yg+huHDZAC!U#3wVsIdX1z*Ayj-Y<7FeBxtm;ZCCE)pbYweqLuT?YT-T*D4*@ z*_8P{ISnJGOQVoIRUs!)->2x(b5GLJYVX|j(N|d%MS2Kt3<0lWA4Ej5 z1AMw3YMIV5Qa-?D#k9L$I7JjvAIRkTQDzfF+((a_IXp>QA4q_$(npE{DnaU_S`(*{ z@<6LF)fr-3%%xw-wka4I;I>fYR8 z;_f!M8j-&i0mhN{&Zx^l_<3<}A@~+zg3dfd_Vo%s3u%3b0kV=vxZ5b}qeY29jV@AD zF6u1!-B@wT6Nd6u=Q8e2g6=|=P=6-jQHjfA$OoO_$nPLEpH+H&x-!Mr+$!9$fF{{l zf^?^Li^BUO|5X#MS8hN70c~>sYtja@ob;?U%4c7DY@k+le0$*f53K_c)jY^UQPOHJ zR5sp641?GskwA`Y(hW&6-2#toZ%P4v2K}?f+#k`pU+T@h;?&{Dc8Ht#k;n`Ptv{g?>O{}Z zpK|!v=MvBWKn(LOb()Ck%w>@3tm2JEW4618T9H3_|2hCqS$~+zs|J;tKqq2@e zco*IP?LJcmquoOUQeDt6{?ISS0A-vc@vftP>;VL5bN{n;k!)!0yM7^lBn1GA5h&_r z2L!M&fH2qwP&cN0bCfSOGtYt8L9Yri_H#4!Hv?5rULxcG?pu#)maY9yTSnCaKY}L_ zR{(yYCH{xT{qMb(nDQa@`Eurt4gxZJgZgWWMg7!~WCDr{($SIfI252_7^I~e~pyzuOK%3;TUQ>Atc5aqObCE{OSDTNCrl4 z8)SSFD%(~%CCF^2Uh3OJ5apsTiGOZDys#5kMTma-4swREOk}_nV?$YT^vlnR`P892 zJ=pkfO$z7I*^<+dr7hk`{f_CMQ zOJ(>rUx&ZlRgsU)Q$|g4by9=h0?SFFi%N>(IYTTC^tZL?_=-yEwNiZdM&M!VgvxK_ zofzh6C_Jt&guqrLV~P`oda@?o<__@8yK>{r10wwxb0e8=(#VT;9&1folb1C-eu~Ql zLw*>vE4nkK)qU5XDM>a_ruWHGRbTV;U^eHqe?% zUvdB;6k!iNY(;*sIesP88NDvQ9sw`0pxGJg1o9>zI5#r8RvH4;Jj^xX!u91hdx(a; zcxHz~4IxvUsc$}^TxA`>2mq25(s^mUXB_ld>DlYFWQJ9;ZXzPWXw39nMiE){nGzS( zIB1-bql=nt?=(>iTJ2G>Q&m5aKQ{q68ofUfg9L@F6hLLYXJA{iI7n+JdUOl-6YU|0 zejSzZ37&zktvw*rgjg2jid26G<+9{O(OdUE@#HwnB}ZwP{gOjkX*r%7&vKJLuH4@d zi_0S;LE7uJlfs6UKoo~_^0E64T65AS`&l5ZdGBicM`yot74GYudj{HGm|e}xQjW$t zZ-$$Im&6gh{~U5Kj5*n z3^%VLF{oq=Ut5b=07~+gLKp}`*a_oRkjJ4A&|g>RiegB~Ep+b478Q`*H{_U+753!f zrBc(im7e&)V5=AZjlnhn>z9FDu2UU9i{)$=1kBtOQ_&WM@a zR=g&6v3~9AYMl(U*Kl4o{~}BY@X6;(_0eq0`aNgIwL9D$(St3}lTjC?PS+74y&1&9 zu1$7`h7FB9PYCJ>I+!x;Q|7JZx~^tP7s7HX+@g=6H!c$i{wblA3^#eMU^PRDSece| zJqgSxH4)6n$6ABjj;l7W^1I??2{9g(J;Md zdIBmo2v$(teiPPL+~e%fg{kVs)H zVzxribze3ZX=&{VVzBNzs4(*yEA@xRam6x=$PoX+;e_C*I@1J4h~`k`^l)U}odc*v zx%zAR3XSA_^UA`5CtF2*!lU;zoM%(4(HRf0VMl}sRA+{jH(=PB!Roc4*vk8b6(fE+ zGip?{SncQ{QJ2Ovm13oxDq&Wg8VI4bYV-vRjtabXc7a)Y+BFz}9L`;!%N7IoYkT8H zmiDi7*nY~GGT+!F!H(|K13XVP7+*1+B>f9PmMrpT02hAxa;Au+f1DN~H?2x&qA3HS z^w*FXMJ{R)lI3B>mc)kAZ&kWoOnn$+$M;NwgP=S7I;rUg(nR8<_H-)p(e#~h(=ilxBd{~Q_y^Q7V?Q@EBZ}8o; z%&AF8IF{X+aRu$D#4#Cb=Le0nuTyb$_0NLSQ%J^T;Km5L% zP~$u3_l6Mr#~BPfsB;ScvVG1+Q%n}v-?ab-@xNnxv=z$nqI1=t3*ZS+^P~PViKtdA zBF9KiaQn2g=cjDrhfdB}>89O$VDGT}MrFb`bNg3PDK8E+U z>R2+3YOt{n%8oWu7DmqTF=P5hK+Cz0t5?U~=QIRKnKxfpa!hYOyA0Ai>L`IMc6ls_ zOqoJPS=6h1fw|B`r!($RiUvK4)E0eI9hAm$>BVz68kHOQ(@zCK%1!l88$&Rjf6#Y@ zdeZyDIY-{br(I{6QLvhZJ|AY&s0;L};7IQFGZ?0jGnEsOpd;DoGe8ijD@`7Le0T32 zTiGMU@D-mXS2^KM5T|rUxh=B+n+1RVku>;@Hw~rKCskI~dVK~Z!u3FzHMfb}hDnZW zO_z<&_Z}EVh_nx?+g$Vyh+g_>)CF4x6|+sYP$UZ~Ze)I= zu^^@Dx^#!~CmOE{Y{2uwVaLqWH>*-YREjeRgca{QA%toq|7LF!iJZzA7}~3s_+^W* zZ|7k*o!6!Ec#W`CryGnJhxTiq9>f|#Ghehn`L1D$GQGfA+!CM8irE};EN^%Xu1jQm zoBP%cas^PcsuA{W1DYqmIMu$l4cji-Y0hvw#nNX?w9fK^CtN4r7jVs&Ad3$~A67mN+bn@~O4>s3L3eB)A64V43m!DX@@5G+Y^2T( zxW8N&#Og7NC(ZLNju5lOnkDUVvzJ<0g}z049gZrHV0d3`OyriR(!;ZIb{RVMv;ZWI+-CtxkrLM~QN2xz z<%ZbQ_sCDq%c(2S6|7v76062Y2m71$(w>DzuB+D9rw%5haUs2V^`<1~Lm~>u`B}=~ zm*N(qTKf8lKKs7mh&(cHQ5ifTQP{&4mqO9$>jV1g{Mr~=o@@hX;5>SGdh@<1f~Xi!$zyLvEN3}*b!5|e3Y5k${Z{v$W9Z>+?A;*BG6|~D z{0f)a;=GnPVi?^ZDLlGhlhQN|O+B`eg2epkm@OxtH@H`$-prE0^zD0Ep}_(SZL((} zC*Z3TwwE4G21@nY6>HI>#eJoB(2tAlG9TKvxf0iS=MhK^xUte$k9%xDN8P$8g@h{t z<;Ph zsAox|)3B|dVp^GA3j6(niutY@nsr~iePt0SO_t@*C4_p%S*QzlTHrE2k9=wO zB_iV`;&tSU$8nqH>c@`Vo=xjgimD?n33D|4XlZ5lOWXk2 z#^p?O=u0(y*9S=m3FYsgCxrmOSbC~-D9$RE0^YJ{;qY~?bFJq87pLKuqrSb-|BK#3 z+ViiBMj#Py9F3+7(t5aLbKlGMom^=EAgZ|IiYLWvN7kji;#;%OVyR%}@2he%c_WS)yUCD(*dB5L zGh<^64)9Vd83E z-!;2gu|*+;9<*s_&_p{}b~-F&pqs2@vJ#ERvtroJR?8u@=g(M6T+5)#bkxFQ_9&kZ zzCC~vF2nX1EiO=Bcw}}oO=&hSdo49QWe6ApFk4n)9d?= znMq!H@V1bg^%WPltv=_7o%Ai0-%w7es_!7xIawYc^YuorBhPlT3@tAlW9TXJ_P7X0 zCbRBf&KF#tv+B?Y zKu0J3i;fO@Z#DG>5Mjov@cdm^&2?aZ>ceVXo~tix7A!?qJN%+(&D~@a z>Jz~$pJJ_K77JD2am%9{WS3l#>x-{1T+ZWW9S$=&{%6)jI>1QOa_-EF=) zEshAu>Ij)Uc~2rlJ?xF^+I3^(HgH!Me=={ zjbjqo^wOge|J!@-L*GI83DdiQH;4+r2$1-{vEX<911yNm{T)kS_=W8F^9xOTsvS-N zfa3;67x$|)PdP8p4PmqQ5He-|YFX{yP-==Czk}d30K8$4hpnKtn1;8YQsE)lz<6EU)OzD*IF<) zgfkoh$1X+hymXlqo6h4L4=6UtpWnF7ng|Nqha_WedmWlgYdU*ISifH>g0q18QU(UrfAOcdZhkKacZV z=lVo0Vxj7yhL_`mg5&F6E*@FB3gJEjHwBds)rpDQT+xqmBax9l!2JsO=F8Z&Fdx(f zO5`Q1k_gem*To7-1uBtJ7$VLpju?(rqf7QJ(Kq|3IG?23Dj(=S4i1yYmD=YLa#fOh zzs{Hq$L*Vlke2!o!m#-2gM;Z%F!|lIn9l>(yC?-Z1h=c}>CMBtTdgW>Gnk33W93Fw zJB(c-nNLCyaPE!NR9Mkt$Q`d)3lR0AqIJ<;@T_x8zQ9)59Lc^B| zkMrjqfKL|vJ^zcj^6QV7=nrfb`Sw1<6^!>6@tGax5Aj(oH|>8Ki$tN%DY510&v>NL zo_J2+?PrO!zy+lPTSY#+!O?DRPE~xZJuFx)LXaaNeSnfdC!OK5=Dw%YVKc5%UZxle z<$57n6?hM_~yC?ltr%2?sZ@vMIs08C!mb%tnGTVt{_z_`toURLg@BP zPBq<3#v^Fk@U159+=k?1Rv}ancmIX|sO&__!f-J*?~tR$W8#wtDnC~h?2m3~4fbE> zGLQz}Z_npcrRpb}XxnkJ9U71Df?dLqqLQP{ggg3CsE_xGv13IBDcM8IkOkH}!*P9^ z`mdR4Bwh8G%AD?qzC8TYd?nNvqx0xE8r7@?`5AsM_kj58+qS*-)2eKv!x}k7rf3>* zoqgp{C$KuYPTuOqoig7qcu%hJRUzZ;KVr^_XUIhP2{L+e{-eUu2tLvWPpTh@xW2r$ zeYa3MTt%%#O)Qd1vM)jf8!WBsjIX%xc>5HYLl|y;c{;YC?izu|W4}He#+YU=zOK*) z``ia$Ra9R~w5q+6m=s%9heVR1qTwz;N~eInK}FXFtDpy8>O4DtioMcNDn1Ftt}Y|EnCjy7Nh!gJ(fKR@h!P&sE)%M1 zQ2*rqgZ@qH$U z#E(42NgUQ+=hdkej@5ZFcAIt$YwU$?sCP2_a=BajD*_0DRn2xU%I>jZc%hJDj| z9x4Kl)pnGn=5luFQ(RxD-7fSXX6nh)lQ9(`sg=*N82#bNNp_0CHKGdjTA^f+c)9C3 zvM8*+cB6Q=+0Z)3J=1Dtfdu>Ak`*7M%y(`y^yKDuTVnd8dvropn4V}y2-KF5UhGPs zTp+291107G_kkL-^2}4qA(lxE+V#LCv$srDg6KN=mmsDF!RHNt|3AW{cB^aK9>TJh zuI+K0*Qd%?XRzL=2SyjWndd;sq6nRwFzP0wfh5GU!ka7cb=lvyDN}_+8&tx9P#TC$ zZZf~-?jH`fz}2#Xf)%Z(e(XkL00f7|KYKC%u65bp!be;Ae}tA(@qfr51Nb76ks9!b z-DhuHU~t=>KloGsS|Z6$d5Qloe9E#YxGX*X`MBQ~Kk}Gw;ZeO+PW(*p29^!^b#zf~ zLv~Y!kT5eQT|I$l%9^&dwxut4dwP-;S&|>gx>%I$i+TwizKTsYpqFeFK7rkDaii@i zGANrlR*aJfEqPm0n${*M)Uo?8W~NR81VRHHKorxLdyS=UEx!xyOg$h_Y@r2#-(c0D zaACSSyLxQ}1u`er#~IutJK{{{X+H+iM?Uy-W_v@#dwq`nhh&)W>!F!e6g8*BeC9_&A`cL)Hp|B* z1(3|mc#^kyxyIxiXwTfn#N|U)HQrg9$0{s3%SUmehK@NUg3?owRFH(1^hqc$gr&CR zD#~CTHv2qk5PL%U zp(w^xV)#W+q}+h9vL4$#!R-oLkzz-J1?XpqJImGHj-&IO@1tW6T0#6bUm!S!HCs$2~c2uzD6}F}1ts5Y(vOD&u*lNNUS* z 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

    =====Template Start=====

    + +

    Current Document Info:

    +Name: ${document.name}
    +Ref: ${document.nodeRef}
    +Type: ${document.type}
    +Content URL:
    /alfresco${document.url}
    +Locked: <#if document.isLocked>Yes<#else>No
    +Aspects: + +<#list document.aspects as aspect> + + +
    ${aspect}
    + +

    =====Template End=====

    diff --git a/config/alfresco/templates/content/examples/example.ftl b/config/alfresco/templates/content/examples/example.ftl new file mode 100644 index 0000000000..28628aa543 --- /dev/null +++ b/config/alfresco/templates/content/examples/example.ftl @@ -0,0 +1,47 @@ +

    =====Example Template Start=====

    + +Company Home Space: ${companyhome.properties.name} +
    +My Home Space: ${userhome.properties.name} +
    +Company Home children count: ${companyhome.children?size} +
    +Company Home first child node name: ${companyhome.children[0].properties.name} +
    +Current Document Name: ${document.name} +
    +Current Space Name: ${space.name} + +

    List of child spaces in my Home Space:

    + +<#list userhome.children as child> + <#if child.isContainer> + + + + + + + +
    ${child.properties.name} (${child.children?size})Path: ${child.displayPath}
    + +

    List of docs in my Home Space (text only content shown inline, JPG images shown as thumbnails):

    + +<#list userhome.children as child> + <#if child.isDocument> + + <#if child.mimetype = "text/plain"> + + <#elseif child.mimetype = "image/jpeg"> + + + + +
    ${child.properties.name}
    ${child.content}
    + +

    Assoc example:

    +<#if userhome.children[0].assocs["cm:contains"]?exists> + ${userhome.children[0].assocs["cm:contains"][0].name} + + +

    =====Example Template End=====

    diff --git a/config/alfresco/templates/content/examples/localizable.ftl b/config/alfresco/templates/content/examples/localizable.ftl new file mode 100644 index 0000000000..12cf233a83 --- /dev/null +++ b/config/alfresco/templates/content/examples/localizable.ftl @@ -0,0 +1,10 @@ +<#-- Shows if a document is localizable and the locale if set --> +Localisable: +<#if hasAspect(document, "cm:localizable") = 1> + Yes
    + <#if document.properties.locale?exists> + Locale: ${document.properties.locale.properties.name} + +<#else> + No
    + diff --git a/config/alfresco/templates/content/examples/my_docs.ftl b/config/alfresco/templates/content/examples/my_docs.ftl new file mode 100644 index 0000000000..1a31125b52 --- /dev/null +++ b/config/alfresco/templates/content/examples/my_docs.ftl @@ -0,0 +1,20 @@ +<#-- Table of the documents in my Home Space --> +<#-- Shows the Icon and link to the content for the doc, also the size in KB and lock status --> + + + + + + + + <#list userhome.children as child> + <#if child.isDocument> + + + + + + + + +
    NameSizeLocked
    ${child.properties.name}${(child.size / 1000)?string("0.##")} KB <#if child.isLocked>Yes
    diff --git a/config/alfresco/templates/content/examples/my_pressreleases.ftl b/config/alfresco/templates/content/examples/my_pressreleases.ftl new file mode 100644 index 0000000000..1fa8d9c825 --- /dev/null +++ b/config/alfresco/templates/content/examples/my_pressreleases.ftl @@ -0,0 +1,22 @@ +<#-- Displays a table of all the documents from a "Press Releases" folder under Company Home --> +<#-- Obviously this folder needs to exist and the docs in it should have the title and description fields set --> + + <#list companyhome.children as child> + <#if child.isContainer && child.name = "Press Releases"> + <#list child.children as doc> + <#if doc.isDocument> + + + + + + + + + + + + + + +

    ${doc.properties.title}

    ${doc.properties.description}
    ${doc.content}
    diff --git a/config/alfresco/templates/content/examples/my_spaces.ftl b/config/alfresco/templates/content/examples/my_spaces.ftl new file mode 100644 index 0000000000..f83005cab4 --- /dev/null +++ b/config/alfresco/templates/content/examples/my_spaces.ftl @@ -0,0 +1,15 @@ +<#-- Table of the Spaces in my Home Folder --> +<#-- Shows the large 32x32 pixel icon, and generates an external access servlet URL to the space --> + + <#list userhome.children as child> + <#if child.isContainer> + + + <#assign ref=child.nodeRef> + <#assign workspace=ref[0..ref?index_of("://")-1]> + <#assign storenode=ref[ref?index_of("://")+3..]> + + + + +
    ${child.properties.name} (${child.children?size})
    diff --git a/config/alfresco/templates/content/examples/my_summary.ftl b/config/alfresco/templates/content/examples/my_summary.ftl new file mode 100644 index 0000000000..c2803041f9 --- /dev/null +++ b/config/alfresco/templates/content/examples/my_summary.ftl @@ -0,0 +1,8 @@ +<#-- Table of some summary details about the current user --> + + + + + + +
    Name: ${person.properties.firstName} ${person.properties.lastName}
    User: ${person.properties.userName}
    Home Space location: ${userhome.displayPath}/${userhome.name}
    Items in Home Space: ${userhome.children?size}
    Items in Company Space: ${companyhome.children?size}
    diff --git a/config/alfresco/templates/content/examples/translatable.ftl b/config/alfresco/templates/content/examples/translatable.ftl new file mode 100644 index 0000000000..f47dc69d98 --- /dev/null +++ b/config/alfresco/templates/content/examples/translatable.ftl @@ -0,0 +1,12 @@ +<#-- Shows the translations applied to a doc through the translatable aspect --> +Translatable: +<#if hasAspect(document, "cm:translatable") = 1> + Yes
    + + <#list document.assocs["cm:translations"] as t> + + +
    ${t.content}
    +<#else> + No
    + diff --git a/config/alfresco/templates/content/examples/userhome_docs.ftl b/config/alfresco/templates/content/examples/userhome_docs.ftl new file mode 100644 index 0000000000..180fb92aa7 --- /dev/null +++ b/config/alfresco/templates/content/examples/userhome_docs.ftl @@ -0,0 +1,15 @@ +<#-- List of docs in the Home Space for current user --> +<#-- If the doc mimetype is plain/text then the content is shown inline --> +<#-- If the doc mimetype is JPEG then the image is shown inline as a small thumbnail image --> + +<#list userhome.children as child> + <#if child.isDocument> + + <#if child.mimetype = "text/plain"> + + <#elseif child.mimetype = "image/jpeg"> + + + + +
    ${child.properties.name}
    ${child.content}
    diff --git a/config/alfresco/templates/content_template_examples.xml b/config/alfresco/templates/content_template_examples.xml new file mode 100644 index 0000000000..4889b16653 --- /dev/null +++ b/config/alfresco/templates/content_template_examples.xml @@ -0,0 +1,213 @@ + + + + + + + + + + admin + 2005-10-21T15:11:00.103+01:00 + company_logos.ftl + 8138e003-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/company_logos.ftl|mimetype=text/plain|size=690|encoding=UTF-8 + company_logos.ftl + company_logos.ftl + SpacesStore + 2005-10-21T15:10:59.509+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:00.900+01:00 + doc_info.ftl + 81d1768e-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/doc_info.ftl|mimetype=text/plain|size=577|encoding=UTF-8 + doc_info.ftl + doc_info.ftl + SpacesStore + 2005-10-21T15:11:00.446+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:01.759+01:00 + example.ftl + 8243e77b-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/example.ftl|mimetype=text/plain|size=1577|encoding=UTF-8 + example.ftl + example.ftl + SpacesStore + 2005-10-21T15:11:01.196+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:02.743+01:00 + localizable.ftl + 82d7c318-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/localizable.ftl|mimetype=text/plain|size=293|encoding=UTF-8 + localizable.ftl + localizable.ftl + SpacesStore + 2005-10-21T15:11:02.181+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:03.587+01:00 + my_docs.ftl + 83692db5-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/my_docs.ftl|mimetype=text/plain|size=750|encoding=UTF-8 + my_docs.ftl + my_docs.ftl + SpacesStore + 2005-10-21T15:11:03.118+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:04.337+01:00 + my_pressreleases.ftl + 83db9ea2-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/my_pressreleases.ftl|mimetype=text/plain|size=910|encoding=UTF-8 + my_pressreleases.ftl + my_pressreleases.ftl + SpacesStore + 2005-10-21T15:11:03.868+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:05.150+01:00 + my_spaces.ftl + 84553b7f-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/my_spaces.ftl|mimetype=text/plain|size=682|encoding=UTF-8 + my_spaces.ftl + my_spaces.ftl + SpacesStore + 2005-10-21T15:11:04.665+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:05.994+01:00 + my_summary.ftl + 84d3934c-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/my_summary.ftl|mimetype=text/plain|size=537|encoding=UTF-8 + my_summary.ftl + my_summary.ftl + SpacesStore + 2005-10-21T15:11:05.509+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:06.759+01:00 + translatable.ftl + 854f7a19-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/translatable.ftl|mimetype=text/plain|size=322|encoding=UTF-8 + translatable.ftl + translatable.ftl + SpacesStore + 2005-10-21T15:11:06.306+01:00 + + + + + + + + + + + admin + 2005-10-21T15:11:07.525+01:00 + userhome_docs.ftl + 85c45b07-423c-11da-be9d-bb84ac6911f1 + admin + workspace + contentUrl=classpath:alfresco/templates/content/examples/userhome_docs.ftl|mimetype=text/plain|size=652|encoding=UTF-8 + userhome_docs.ftl + userhome_docs.ftl + SpacesStore + 2005-10-21T15:11:07.072+01:00 + + + + \ No newline at end of file diff --git a/config/alfresco/templates/software_engineering_project.xml b/config/alfresco/templates/software_engineering_project.xml new file mode 100644 index 0000000000..e920f4fe21 --- /dev/null +++ b/config/alfresco/templates/software_engineering_project.xml @@ -0,0 +1,69 @@ + + + + + ${templates.space.project} + space-icon-default + + + + ${templates.space.documentation} + space-icon-default + + + + ${templates.space.drafts} + space-icon-default + + + + ${templates.space.pending_approval} + space-icon-default + + + + ${templates.space.published} + space-icon-default + + + + ${templates.space.samples} + space-icon-doc + + + + ${templates.document.system_overview.title} + ${templates.document.system_overview.name} + ${templates.document.system_overview.name} + contentUrl=classpath:alfresco/templates/${templates.document.system_overview.name}|mimetype=text/html|size=|encoding= + + + + + + + + ${templates.space.discussions} + space-icon-default + + + + ${templates.space.ui_design} + space-icon-default + + + + ${templates.space.presentations} + space-icon-default + + + + ${templates.space.quality_assurance} + space-icon-default + + + + + diff --git a/config/alfresco/templates/system-overview.html b/config/alfresco/templates/system-overview.html new file mode 100644 index 0000000000..5c1be6723a --- /dev/null +++ b/config/alfresco/templates/system-overview.html @@ -0,0 +1,18 @@ + + +System Overview + + +

    System Overview

    +

    Purpose

    +In this section, give a brief summary of the purpose of the proposed system, +including the target users and organisations. +

    Aims and Objectives

    +In this section, give a brief summary of the reasons for developing the +proposed system. +

    Prerequisites

    +In this section give any prerequisites for the proposed system. +

    Total Expected Effort

    +In this section give a rough estimate in months of effort required. + + diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties new file mode 100644 index 0000000000..03a1606f19 --- /dev/null +++ b/config/alfresco/version.properties @@ -0,0 +1,14 @@ +# +# Community network version information +# + +# Version label + +version.major=1 +version.minor=2 +version.revision=0 +version.label=dev + +# Edition label + +version.edition=Open Source diff --git a/config/ehcache.xml b/config/ehcache.xml new file mode 100644 index 0000000000..6a7df93a63 --- /dev/null +++ b/config/ehcache.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/treecache.xml b/config/treecache.xml new file mode 100644 index 0000000000..708ef2ba71 --- /dev/null +++ b/config/treecache.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + READ_COMMITTED + + + REPL_SYNC + + + Alfresco-Hibernate-Cluster + + + + + + + + + + # + + + + + + + + + + + + + 20000 + + + 10000 + + + 15000 + + + + + + + diff --git a/project-build.xml b/project-build.xml new file mode 100644 index 0000000000..10ae346210 --- /dev/null +++ b/project-build.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + ${javadoc.copyright} + + + + diff --git a/project-override.properties b/project-override.properties new file mode 100644 index 0000000000..2babc68ee5 --- /dev/null +++ b/project-override.properties @@ -0,0 +1,2 @@ +javadoc.title.window=Alfresco Repository +javadoc.title.document=Alfresco Repository Specification \ No newline at end of file diff --git a/project.properties b/project.properties new file mode 100644 index 0000000000..f27368e16c --- /dev/null +++ b/project.properties @@ -0,0 +1,2 @@ +file.jibx.binding=${dir.src.java}/org/alfresco/repo/dictionary/m2binding.xml +dir.javadoc.api.service=${dir.docs}/java-public-service-api diff --git a/source/java/org/alfresco/example/SimpleExampleWithContent.java b/source/java/org/alfresco/example/SimpleExampleWithContent.java new file mode 100644 index 0000000000..5025e3ed7b --- /dev/null +++ b/source/java/org/alfresco/example/SimpleExampleWithContent.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.example; + +import java.io.File; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.TempFileProvider; +import org.alfresco.util.debug.NodeStoreInspector; +import org.springframework.context.ApplicationContext; + +/** + * A quick example of how to + *
      + *
    • get hold of the repository service
    • + *
    • initialise a model
    • + *
    • create nodes
    • + *
    • load in some content
    • + *
    + *

    + * + * All the normal checks for missing resources and so forth have been left out in the interests + * of clarity of demonstration. + * + *

    + * To change the model being used, make changes to the dictionaryDAO bean in the + * application contenxt XML file. For now, this example is written against the + * generic alfresco/model/contentModel.xml. + *

    + * The content store location can also be set in the application context. + * + * + * @author Derek Hulley + */ +public class SimpleExampleWithContent +{ + private static final String NAMESPACE = "http://www.alfresco.org/test/SimpleExampleWithContent"; + + public static void main(String[] args) + { + // initialise app content + ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + // get registry of services + final ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + + // begin a UserTransaction + // All the services are set to create or propogate the transaction. + // This transaction will be recognised and propogated + // The TransactionUtil takes care of the catching and rollback, etc + TransactionService transactionService = serviceRegistry.getTransactionService(); + TransactionWork exampleWork = new TransactionWork() + { + public Object doWork() throws Exception + { + doExample(serviceRegistry); + return null; + } + }; + TransactionUtil.executeInUserTransaction(transactionService, exampleWork); + System.exit(0); + } + + private static void doExample(ServiceRegistry serviceRegistry) throws Exception + { + // get individual, required services + NodeService nodeService = serviceRegistry.getNodeService(); + ContentService contentService = serviceRegistry.getContentService(); + + // create a store, if one doesn't exist + StoreRef storeRef = new StoreRef( + StoreRef.PROTOCOL_WORKSPACE, + "SimpleExampleWithContent-" + GUID.generate()); + if (!nodeService.exists(storeRef)) + { + nodeService.createStore(storeRef.getProtocol(), storeRef.getIdentifier()); + } + + // get the root node from which to hang the next level of nodes + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + + Map nodeProperties = new HashMap(7); + + // add a simple folder to the root node + nodeProperties.clear(); + nodeProperties.put(ContentModel.PROP_NAME, "My First Folder"); + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NAMESPACE, QName.createValidLocalName("My First Folder")), + ContentModel.TYPE_FOLDER, + nodeProperties); + NodeRef folderRef = assocRef.getChildRef(); + + // create a file + nodeProperties.clear(); + nodeProperties.put(ContentModel.PROP_NAME, "My First File"); + assocRef = nodeService.createNode( + folderRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NAMESPACE, QName.createValidLocalName("My First File")), + ContentModel.TYPE_CONTENT, + nodeProperties); + NodeRef fileRef = assocRef.getChildRef(); + + ContentWriter writer = contentService.getWriter(fileRef, ContentModel.PROP_CONTENT, true); + // the mimetype will up pushed onto the node automatically once the stream closes + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + // store string content as UTF-8 + writer.setEncoding("UTF-8"); + + // write some content - this API allows streaming and direct loading, + // but for now we'll just upload a string + // The writer, being updating, will take care of updating the node once the stream + // closes. + String content = "The quick brown fox jumps over the lazy dog"; + writer.putContent(content); + + // dump the content to a file + File file = TempFileProvider.createTempFile("sample", ".txt"); + ContentReader reader = contentService.getReader(fileRef, ContentModel.PROP_CONTENT); + reader.getContent(file); + + // just to demonstrate the node structure, dump it to the file + String dump = NodeStoreInspector.dumpNodeStore(nodeService, storeRef); + System.out.println("Node Store: \n" + dump); + + // and much, much more ... + } +} diff --git a/source/java/org/alfresco/filesys/CIFSServer.java b/source/java/org/alfresco/filesys/CIFSServer.java new file mode 100644 index 0000000000..59adab7779 --- /dev/null +++ b/source/java/org/alfresco/filesys/CIFSServer.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.SocketException; +import java.util.Vector; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.netbios.server.NetBIOSNameServer; +import org.alfresco.filesys.server.NetworkServer; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.smb.server.SMBServer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * CIFS Server Class + * + *

    Create and start the various server components required to run the CIFS server. + * + * @author GKSpencer + */ +public class CIFSServer +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.server"); + + // Server configuration + + private ServerConfiguration filesysConfig; + + // List of CIFS server components + + private Vector serverList = new Vector(); + + /** + * Class constructor + * + * @param serverConfig ServerConfiguration + */ + public CIFSServer(ServerConfiguration serverConfig) + { + this.filesysConfig = serverConfig; + } + + /** + * Return the server configuration + * + * @return ServerConfiguration + */ + public final ServerConfiguration getConfiguration() + { + return filesysConfig; + } + + /** + * @return Returns true if the server started up without any errors + */ + public boolean isStarted() + { + return (filesysConfig != null && filesysConfig.isSMBServerEnabled()); + } + + /** + * Start the CIFS server components + * + * @exception SocketException If a network error occurs + * @exception IOException If an I/O error occurs + */ + public final void startServer() throws SocketException, IOException + { + try + { + // Create the SMB server and NetBIOS name server, if enabled + + if (filesysConfig.isSMBServerEnabled()) + { + // Create the NetBIOS name server if NetBIOS SMB is enabled + + if (filesysConfig.hasNetBIOSSMB()) + serverList.add(new NetBIOSNameServer(filesysConfig)); + + // Create the SMB server + + serverList.add(new SMBServer(filesysConfig)); + + // Add the servers to the configuration + + for (NetworkServer server : serverList) + { + filesysConfig.addServer(server); + } + } + + // Start the CIFS server(s) + + for (NetworkServer server : serverList) + { + if (logger.isInfoEnabled()) + logger.info("Starting server " + server.getProtocolName() + " ..."); + + // Start the server + + String serverName = server.getConfiguration().getServerName(); + server.startServer(); + } + } + catch (Throwable e) + { + filesysConfig = null; + throw new AlfrescoRuntimeException("Failed to start CIFS Server", e); + } + // success + } + + /** + * Stop the CIFS server components + */ + public final void stopServer() + { + if (filesysConfig == null) + { + // initialisation failed + return; + } + + // Shutdown the CIFs server components + + for ( NetworkServer server : serverList) + { + if (logger.isInfoEnabled()) + logger.info("Shutting server " + server.getProtocolName() + " ..."); + + // Stop the server + + server.shutdownServer(false); + + // Remove the server from the global list + + getConfiguration().removeServer(server.getProtocolName()); + } + + // Clear the server list and configuration + + serverList.clear(); + filesysConfig = null; + } + + /** + * Runs the CIFS server directly + * + * @param args String[] + */ + public static void main(String[] args) + { + PrintStream out = System.out; + + out.println("CIFS Server Test"); + out.println("----------------"); + + try + { + // Create the configuration service in the same way that Spring creates it + + ApplicationContext ctx = new ClassPathXmlApplicationContext("alfresco/application-context.xml"); + + // Get the CIFS server bean + + CIFSServer server = (CIFSServer) ctx.getBean("cifsServer"); + if (server == null) + { + throw new AlfrescoRuntimeException("Server bean 'cifsServer' not defined"); + } + + // Stop the FTP server, if running + + server.getConfiguration().setFTPServerEnabled(false); + + NetworkServer srv = server.getConfiguration().findServer("FTP"); + if ( srv != null) + srv.shutdownServer(true); + + // Only wait for shutdown if the SMB/CIFS server is enabled + + if ( server.getConfiguration().isSMBServerEnabled()) + { + + // SMB/CIFS server should have automatically started + // Wait for shutdown via the console + + out.println("Enter 'x' to shutdown ..."); + boolean shutdown = false; + + // Wait while the server runs, user may stop the server by typing a key + + while (shutdown == false) + { + + // Wait for the user to enter the shutdown key + + int ch = System.in.read(); + + if (ch == 'x' || ch == 'X') + shutdown = true; + + synchronized (server) + { + server.wait(20); + } + } + + // Stop the server + + server.stopServer(); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + System.exit(1); + } +} diff --git a/source/java/org/alfresco/filesys/FTPServer.java b/source/java/org/alfresco/filesys/FTPServer.java new file mode 100644 index 0000000000..cf3a10d913 --- /dev/null +++ b/source/java/org/alfresco/filesys/FTPServer.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.SocketException; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.ftp.FTPNetworkServer; +import org.alfresco.filesys.server.NetworkServer; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * FTP Server Class + * + *

    Create and start the server components required to run the FTP server. + * + * @author GKSpencer + */ +public class FTPServer +{ + private static final Log logger = LogFactory.getLog("org.alfresco.ftp.server"); + + // Server configuration + + private ServerConfiguration filesysConfig; + + // The actual FTP server + + private FTPNetworkServer ftpServer; + + /** + * Class constructor + * + * @param serverConfig ServerConfiguration + */ + public FTPServer(ServerConfiguration serverConfig) + { + this.filesysConfig = serverConfig; + } + + /** + * Return the server configuration + * + * @return ServerConfiguration + */ + public final ServerConfiguration getConfiguration() + { + return filesysConfig; + } + + /** + * @return Returns true if the server started up without any errors + */ + public boolean isStarted() + { + return (filesysConfig != null && filesysConfig.isFTPServerEnabled()); + } + + /** + * Start the FTP server components + * + * @exception SocketException If a network error occurs + * @exception IOException If an I/O error occurs + */ + public final void startServer() throws SocketException, IOException + { + try + { + // Create the FTP server, if enabled + + if (filesysConfig.isFTPServerEnabled()) + { + // Create the FTP server + + ftpServer = new FTPNetworkServer(filesysConfig); + filesysConfig.addServer(ftpServer); + } + + // Start the FTP server + + if (logger.isInfoEnabled()) + logger.info("Starting server " + ftpServer.getProtocolName() + " ..."); + + // Start the server + + ftpServer.startServer(); + } + catch (Throwable e) + { + filesysConfig = null; + throw new AlfrescoRuntimeException("Failed to start FTP Server", e); + } + // success + } + + /** + * Stop the FTP server components + */ + public final void stopServer() + { + if (filesysConfig == null) + { + // initialisation failed + return; + } + + // Shutdown the FTP server + + if ( ftpServer != null) + { + if (logger.isInfoEnabled()) + logger.info("Shutting server " + ftpServer.getProtocolName() + " ..."); + + // Stop the server + + ftpServer.shutdownServer(false); + + // Remove the server from the global list + + getConfiguration().removeServer(ftpServer.getProtocolName()); + ftpServer = null; + } + + // Clear the configuration + + filesysConfig = null; + } + + /** + * Runs the FTP server directly + * + * @param args String[] + */ + public static void main(String[] args) + { + PrintStream out = System.out; + + out.println("FTP Server Test"); + out.println("---------------"); + + try + { + // Create the configuration service in the same way that Spring creates it + + ApplicationContext ctx = new ClassPathXmlApplicationContext("alfresco/application-context.xml"); + + // Get the FTP server bean + + FTPServer server = (FTPServer) ctx.getBean("ftpServer"); + if (server == null) + { + throw new AlfrescoRuntimeException("Server bean 'ftpServer' not defined"); + } + + // Stop the CIFS server components, if running + + NetworkServer srv = server.getConfiguration().findServer("SMB"); + if ( srv != null) + srv.shutdownServer(true); + + srv = server.getConfiguration().findServer("NetBIOS"); + if ( srv != null) + srv.shutdownServer(true); + + // Only wait for shutdown if the FTP server is enabled + + if ( server.getConfiguration().isFTPServerEnabled()) + { + + // FTP server should have automatically started + // + // Wait for shutdown via the console + + out.println("Enter 'x' to shutdown ..."); + boolean shutdown = false; + + // Wait while the server runs, user may stop the server by typing a key + + while (shutdown == false) + { + + // Wait for the user to enter the shutdown key + + int ch = System.in.read(); + + if (ch == 'x' || ch == 'X') + shutdown = true; + + synchronized (server) + { + server.wait(20); + } + } + + // Stop the server + + server.stopServer(); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + System.exit(1); + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPCommand.java b/source/java/org/alfresco/filesys/ftp/FTPCommand.java new file mode 100644 index 0000000000..ab39eea915 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPCommand.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +/** + * FTP Command Types Class + * + * @author GKSpencer + */ +public class FTPCommand +{ + + // Command ids + + public final static int User = 0; + public final static int Pass = 1; + public final static int Acct = 2; + public final static int Cwd = 3; + public final static int Cdup = 4; + public final static int Smnt = 5; + public final static int Rein = 6; + public final static int Quit = 7; + public final static int Port = 8; + public final static int Pasv = 9; + public final static int Type = 10; + public final static int Stru = 11; + public final static int Mode = 12; + public final static int Retr = 13; + public final static int Stor = 14; + public final static int Stou = 15; + public final static int Appe = 16; + public final static int Allo = 17; + public final static int Rest = 18; + public final static int Rnfr = 19; + public final static int Rnto = 20; + public final static int Abor = 21; + public final static int Dele = 22; + public final static int Rmd = 23; + public final static int Mkd = 24; + public final static int Pwd = 25; + public final static int List = 26; + public final static int Nlst = 27; + public final static int Site = 28; + public final static int Syst = 29; + public final static int Stat = 30; + public final static int Help = 31; + public final static int Noop = 32; + public final static int Mdtm = 33; + public final static int Size = 34; + public final static int Opts = 35; + public final static int Feat = 36; + public final static int XPwd = 37; + public final static int XMkd = 38; + public final static int XRmd = 39; + public final static int XCup = 40; + public final static int XCwd = 41; + + public final static int MaxId = 41; + + public final static int InvalidCmd = -1; + + // Command name strings + + private static final String[] _cmds = { "USER", "PASS", "ACCT", "CWD", "CDUP", "SMNT", "REIN", "QUIT", "PORT", + "PASV", "TYPE", "STRU", "MODE", "RETR", "STOR", "STOU", "APPE", "ALLO", "REST", "RNFR", "RNTO", "ABOR", + "DELE", "RMD", "MKD", "PWD", "LIST", "NLST", "SITE", "SYST", "STAT", "HELP", "NOOP", "MDTM", "SIZE", + "OPTS", "FEAT", "XPWD", "XMKD", "XRMD", "XCUP", "XCWD" }; + + /** + * Convert an FTP command to an id + * + * @param cmd String + * @return int + */ + public final static int getCommandId(String cmd) + { + + // Check if the command is valid + + if (cmd == null) + return InvalidCmd; + + // Convert to a command id + + for (int i = 0; i <= MaxId; i++) + if (_cmds[i].equalsIgnoreCase(cmd)) + return i; + + // Command not found + + return InvalidCmd; + } + + /** + * Return the FTP command name for the specified id + * + * @param id int + * @return String + */ + public final static String getCommandName(int id) + { + if (id < 0 || id > MaxId) + return null; + return _cmds[id]; + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPDataSession.java b/source/java/org/alfresco/filesys/ftp/FTPDataSession.java new file mode 100644 index 0000000000..a0acf05f02 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPDataSession.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +import java.net.*; +import java.io.*; + +/** + * FTP Data Session Class + *

    + * A data connection is made when a PORT or PASV FTP command is received on the main control + * session. + *

    + * The PORT command will actively connect to the specified address/port on the client. The PASV + * command will create a listening socket and wait for the client to connect. + * + * @author GKSpencer + */ +public class FTPDataSession implements Runnable +{ + + // FTP session that this data connection is associated with + + private FTPSrvSession m_cmdSess; + + // Connection details for active connection + + private InetAddress m_clientAddr; + private int m_clientPort; + + // Local port to use + + private int m_localPort; + + // Active data session socket + + private Socket m_activeSock; + + // Passive data session socket + + private ServerSocket m_passiveSock; + + // Adapter to bind the passive socket to + + private InetAddress m_bindAddr; + + // Transfer in progress and abort file transfer flags + + private boolean m_transfer; + private boolean m_abort; + + // Send/receive data byte count + + private long m_bytCount; + + /** + * Class constructor + *

    + * Create a data connection that listens for an incoming connection. + * + * @param sess FTPSrvSession + * @exception IOException + */ + protected FTPDataSession(FTPSrvSession sess) throws IOException + { + + // Set the associated command session + + m_cmdSess = sess; + + // Create a server socket to listen for the incoming connection + + m_passiveSock = new ServerSocket(0, 1, null); + } + + /** + * Class constructor + *

    + * Create a data connection that listens for an incoming connection on the specified network + * adapter and local port. + * + * @param sess FTPSrvSession + * @param localPort int + * @param addr InetAddress + * @exception IOException + */ + protected FTPDataSession(FTPSrvSession sess, int localPort, InetAddress bindAddr) throws IOException + { + + // Set the associated command session + + m_cmdSess = sess; + + // Create a server socket to listen for the incoming connection on the specified network + // adapter + + m_localPort = localPort; + m_passiveSock = new ServerSocket(localPort, 1, bindAddr); + } + + /** + * Class constructor + *

    + * Create a data connection that listens for an incoming connection on the specified network + * adapter. + * + * @param sess FTPSrvSession + * @param addr InetAddress + * @exception IOException + */ + protected FTPDataSession(FTPSrvSession sess, InetAddress bindAddr) throws IOException + { + + // Set the associated command session + + m_cmdSess = sess; + + // Create a server socket to listen for the incoming connection on the specified network + // adapter + + m_passiveSock = new ServerSocket(0, 1, bindAddr); + } + + /** + * Class constructor + *

    + * Create a data connection to the specified client address and port. + * + * @param sess FTPSrvSession + * @param addr InetAddress + * @param port int + */ + protected FTPDataSession(FTPSrvSession sess, InetAddress addr, int port) + { + + // Set the associated command session + + m_cmdSess = sess; + + // Save the client address/port details, the actual connection will be made later when + // the client requests/sends a file + + m_clientAddr = addr; + m_clientPort = port; + } + + /** + * Class constructor + *

    + * Create a data connection to the specified client address and port, using the specified local + * port. + * + * @param sess FTPSrvSession + * @param localPort int + * @param addr InetAddress + * @param port int + */ + protected FTPDataSession(FTPSrvSession sess, int localPort, InetAddress addr, int port) + { + + // Set the associated command session + + m_cmdSess = sess; + + // Save the local port + + m_localPort = localPort; + + // Save the client address/port details, the actual connection will be made later when + // the client requests/sends a file + + m_clientAddr = addr; + m_clientPort = port; + } + + /** + * Return the associated command session + * + * @return FTPSrvSession + */ + public final FTPSrvSession getCommandSession() + { + return m_cmdSess; + } + + /** + * Return the local port + * + * @return int + */ + public final int getLocalPort() + { + if (m_passiveSock != null) + return m_passiveSock.getLocalPort(); + else if (m_activeSock != null) + return m_activeSock.getLocalPort(); + return -1; + } + + /** + * Return the port that was allocated to the data session + * + * @return int + */ + protected final int getAllocatedPort() + { + return m_localPort; + } + + /** + * Return the passive server socket address + * + * @return InetAddress + */ + public final InetAddress getPassiveAddress() + { + if (m_passiveSock != null) + { + + // Get the server socket local address + + InetAddress addr = m_passiveSock.getInetAddress(); + if (addr.getHostAddress().compareTo("0.0.0.0") == 0) + { + try + { + addr = InetAddress.getLocalHost(); + } + catch (UnknownHostException ex) + { + } + } + return addr; + } + return null; + } + + /** + * Return the passive server socket port + * + * @return int + */ + public final int getPassivePort() + { + if (m_passiveSock != null) + return m_passiveSock.getLocalPort(); + return -1; + } + + /** + * Determine if a file transfer is active + * + * @return boolean + */ + public final boolean isTransferActive() + { + return m_transfer; + } + + /** + * Abort an in progress file transfer + */ + public final void abortTransfer() + { + m_abort = true; + } + + /** + * Return the transfer byte count + * + * @return long + */ + public final synchronized long getTransferByteCount() + { + return m_bytCount; + } + + /** + * Return the data socket connected to the client + * + * @return Socket + * @exception IOException + */ + public final Socket getSocket() throws IOException + { + + // Check for a passive connection, get the incoming socket connection + + if (m_passiveSock != null) + m_activeSock = m_passiveSock.accept(); + else + { + if (m_localPort != 0) + { + + // Use the specified local port + + m_activeSock = new Socket(m_clientAddr, m_clientPort, null, m_localPort); + } + else + m_activeSock = new Socket(m_clientAddr, m_clientPort); + } + + // Set the socket to close immediately + + m_activeSock.setSoLinger(false, 0); + m_activeSock.setTcpNoDelay(true); + + // Return the data socket + + return m_activeSock; + } + + /** + * Close the data connection + */ + public final void closeSession() + { + + // If the data connection is active close it + + if (m_activeSock != null) + { + try + { + m_activeSock.close(); + } + catch (Exception ex) + { + } + m_activeSock = null; + } + + // Close the listening socket for a passive connection + + if (m_passiveSock != null) + { + try + { + m_passiveSock.close(); + } + catch (Exception ex) + { + } + m_passiveSock = null; + } + } + + /** + * Run a file send/receive in a seperate thread + */ + public void run() + { + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPDate.java b/source/java/org/alfresco/filesys/ftp/FTPDate.java new file mode 100644 index 0000000000..6e68d691eb --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPDate.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +import java.util.*; + +/** + * FTP Date Utility Class + * + * @author GKSpencer + */ +public class FTPDate +{ + + // Constants + // + // Six months in ticks + + protected final static long SIX_MONTHS = 183L * 24L * 60L * 60L * 1000L; + + // Month names + + protected final static String[] _months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", + "Nov", "Dec" }; + + /** + * Pack a date string in Unix format The format is 'Mmm dd hh:mm' if the file is less than six + * months old, else the format is 'Mmm dd yyyy'. + * + * @param buf StringBuffer + * @param dt Date + */ + public final static void packUnixDate(StringBuffer buf, Date dt) + { + + // Check if the date is valid + + if (dt == null) + { + buf.append("------------"); + return; + } + + // Get the time raw value + + long timeVal = dt.getTime(); + if (timeVal < 0) + { + buf.append("------------"); + return; + } + + // Add the month name and date parts to the string + + Calendar cal = new GregorianCalendar(); + cal.setTime(dt); + buf.append(_months[cal.get(Calendar.MONTH)]); + buf.append(" "); + + int dayOfMonth = cal.get(Calendar.DATE); + if (dayOfMonth < 10) + buf.append(" "); + buf.append(dayOfMonth); + buf.append(" "); + + // If the file is less than six months old we append the file time, else we append the year + + long timeNow = System.currentTimeMillis(); + if (Math.abs(timeNow - timeVal) > SIX_MONTHS) + { + + // Append the year + + buf.append(cal.get(Calendar.YEAR)); + } + else + { + + // Append the file time as hh:mm + + int hr = cal.get(Calendar.HOUR_OF_DAY); + if (hr < 10) + buf.append("0"); + buf.append(hr); + buf.append(":"); + + int sec = cal.get(Calendar.SECOND); + if (sec < 10) + buf.append("0"); + buf.append(sec); + } + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java b/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java new file mode 100644 index 0000000000..ecba21b326 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.Enumeration; + +import org.alfresco.filesys.server.ServerListener; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.alfresco.filesys.server.filesys.NetworkFileServer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

    + * Create an FTP server on the specified port. The default server port is 21. + * + * @author GKSpencer + */ +public class FTPNetworkServer extends NetworkFileServer implements Runnable +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.ftp.protocol"); + + // Constants + // + // Server version + + private static final String ServerVersion = "3.5.0"; + + // Listen backlog for the server socket + + protected static final int LISTEN_BACKLOG = 10; + + // Default FTP server port + + protected static final int SERVER_PORT = 21; + + // Server socket + + private ServerSocket m_srvSock; + + // Active session list + + private FTPSessionList m_sessions; + + // List of available shares + + private SharedDeviceList m_shares; + + // Next available session id + + private int m_sessId; + + // Root path for new sessions + + private FTPPath m_rootPath; + + // FTP server thread + + private Thread m_srvThread; + + // Local server address string, in FTP format (ie. n,n,n,n) + + private String m_localFTPaddress; + + /** + * Class constructor + * + * @param serviceResgistry ServiceRegistry + * @param config ServerConfiguration + */ + public FTPNetworkServer(ServerConfiguration config) + { + super("FTP", config); + + // Set the server version + + setVersion(ServerVersion); + + // Allocate the session lists + + m_sessions = new FTPSessionList(); + + // Enable debug + + if (getConfiguration().getFTPDebug() != 0) + setDebug(true); + + // Create the root path, if configured + + if (getConfiguration().hasFTPRootPath()) + { + + try + { + + // Create the root path + + m_rootPath = new FTPPath(getConfiguration().getFTPRootPath()); + } + catch (InvalidPathException ex) + { + logger.error(ex); + } + } + } + + /** + * Add a new session to the server + * + * @param sess FTPSrvSession + */ + protected final void addSession(FTPSrvSession sess) + { + + // Add the session to the session list + + m_sessions.addSession(sess); + + // Propagate the debug settings to the new session + + if (hasDebug()) + { + + // Enable session debugging, output to the same stream as the server + + sess.setDebug(getConfiguration().getFTPDebug()); + } + } + + /** + * emove a session from the server + * + * @param sess FTPSrvSession + */ + protected final void removeSession(FTPSrvSession sess) + { + + // Remove the session from the active session list + + if (m_sessions.removeSession(sess) != null) + { + + // Inform listeners that a session has closed + + fireSessionClosedEvent(sess); + } + } + + /** + * Allocate a local port for a data session + * + * @param sess FTPSrvSession + * @param remAddr InetAddress + * @param remPort int + * @return FTPDataSession + * @exception IOException + */ + protected final FTPDataSession allocateDataSession(FTPSrvSession sess, InetAddress remAddr, int remPort) + throws IOException + { + // Create a new FTP data session + + FTPDataSession dataSess = null; + if (remAddr != null) + { + + // Create a normal data session + + dataSess = new FTPDataSession(sess, remAddr, remPort); + } + else + { + + // Create a passive data session + + dataSess = new FTPDataSession(sess, getBindAddress()); + } + + // Return the data session + + return dataSess; + } + + /** + * Release a data session + * + * @param dataSess FTPDataSession + */ + protected final void releaseDataSession(FTPDataSession dataSess) + { + + // Close the data session + + dataSess.closeSession(); + } + + /** + * Get the shared device list + * + * @return SharedDeviceList + */ + public final SharedDeviceList getShareList() + { + + // Check if the share list has been populated + + if (m_shares == null) + m_shares = getConfiguration().getShareMapper() + .getShareList(getConfiguration().getServerName(), null, false); + + // Return the share list + + return m_shares; + } + + /** + * Check if the FTP server is to be bound to a specific network adapter + * + * @return boolean + */ + public final boolean hasBindAddress() + { + return getConfiguration().getFTPBindAddress() != null ? true : false; + } + + /** + * Return the address that the FTP server should bind to + * + * @return InetAddress + */ + public final InetAddress getBindAddress() + { + return getConfiguration().getFTPBindAddress(); + } + + /** + * Check if the root path is set + * + * @return boolean + */ + public final boolean hasRootPath() + { + return m_rootPath != null ? true : false; + } + + /** + * Check if anonymous logins are allowed + * + * @return boolean + */ + public final boolean allowAnonymous() + { + return getConfiguration().allowAnonymousFTP(); + } + + /** + * Return the anonymous login user name + * + * @return String + */ + public final String getAnonymousAccount() + { + return getConfiguration().getAnonymousFTPAccount(); + } + + /** + * Return the local FTP server address string in n,n,n,n format + * + * @return String + */ + public final String getLocalFTPAddressString() + { + return m_localFTPaddress; + } + + /** + * Return the next available session id + * + * @return int + */ + protected final synchronized int getNextSessionId() + { + return m_sessId++; + } + + /** + * Return the FTP server port + * + * @return int + */ + public final int getPort() + { + return getConfiguration().getFTPPort(); + } + + /** + * Return the server socket + * + * @return ServerSocket + */ + protected final ServerSocket getSocket() + { + return m_srvSock; + } + + /** + * Return the root path for new sessions + * + * @return FTPPath + */ + public final FTPPath getRootPath() + { + return m_rootPath; + } + + /** + * Notify the server that a user has logged on. + * + * @param sess SrvSession + */ + protected final void sessionLoggedOn(SrvSession sess) + { + + // Notify session listeners that a user has logged on. + + fireSessionLoggedOnEvent(sess); + } + + /** + * Start the SMB server. + */ + public void run() + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + { + logger.debug("FTP Server starting on port " + getPort()); + logger.debug("Version " + isVersion()); + } + + // Create a server socket to listen for incoming FTP session requests + + try + { + + // Create the server socket to listen for incoming FTP session requests + + if (hasBindAddress()) + m_srvSock = new ServerSocket(getPort(), LISTEN_BACKLOG, getBindAddress()); + else + m_srvSock = new ServerSocket(getPort(), LISTEN_BACKLOG); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + { + String ftpAddr = "ALL"; + + if (hasBindAddress()) + ftpAddr = getBindAddress().getHostAddress(); + logger.debug("FTP Binding to local address " + ftpAddr); + } + + // If a bind address is set then we can set the FTP local address + + if (hasBindAddress()) + m_localFTPaddress = getBindAddress().getHostAddress().replace('.', ','); + + // Indicate that the server is active + + setActive(true); + fireServerEvent(ServerListener.ServerActive); + + // Wait for incoming connection requests + + while (hasShutdown() == false) + { + + // Wait for a connection + + Socket sessSock = getSocket().accept(); + + // Set the local address string in FTP format (n,n,n,n), if not already set + + if (m_localFTPaddress == null) + { + if (sessSock.getLocalAddress() != null) + m_localFTPaddress = sessSock.getLocalAddress().getHostAddress().replace('.', ','); + } + + // Set socket options + + sessSock.setTcpNoDelay(true); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("FTP session request received from " + + sessSock.getInetAddress().getHostAddress()); + + // Create a server session for the new request, and set the session id. + + FTPSrvSession srvSess = new FTPSrvSession(sessSock, this); + srvSess.setSessionId(getNextSessionId()); + srvSess.setUniqueId("FTP" + srvSess.getSessionId()); + srvSess.setDebugPrefix("[FTP" + srvSess.getSessionId() + "] "); + + // Initialize the root path for the new session, if configured + + if (hasRootPath()) + srvSess.setRootPath(getRootPath()); + + // Add the session to the active session list + + addSession(srvSess); + + // Inform listeners that a new session has been created + + fireSessionOpenEvent(srvSess); + + // Start the new session in a seperate thread + + Thread srvThread = new Thread(srvSess); + srvThread.setDaemon(true); + srvThread.setName("Sess_FTP" + srvSess.getSessionId() + "_" + + sessSock.getInetAddress().getHostAddress()); + srvThread.start(); + + // Sleep for a while + + try + { + Thread.sleep(1000L); + } + catch (InterruptedException ex) + { + } + } + } + catch (SocketException ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + { + logger.error("FTP Socket error", ex); + + // Inform listeners of the error, store the exception + + setException(ex); + fireServerEvent(ServerListener.ServerError); + } + } + catch (Exception ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + { + logger.error("FTP Server error", ex); + } + + // Inform listeners of the error, store the exception + + setException(ex); + fireServerEvent(ServerListener.ServerError); + } + + // Close the active sessions + + Enumeration enm = m_sessions.enumerate(); + + while (enm.hasMoreElements()) + { + + // Get the session id and associated session + + Integer sessId = (Integer) enm.nextElement(); + FTPSrvSession sess = m_sessions.findSession(sessId); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("FTP Close session, id = " + sess.getSessionId()); + + // Close the session + + sess.closeSession(); + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("FTP Server shutting down ..."); + + // Indicate that the server has shutdown, inform listeners + + setActive(false); + fireServerEvent(ServerListener.ServerShutdown); + } + + /** + * Shutdown the FTP server + * + * @param immediate boolean + */ + public void shutdownServer(boolean immediate) + { + + // Set the shutdown flag + + setShutdown(true); + + // Close the FTP server listening socket to wakeup the main FTP server thread + + try + { + if (getSocket() != null) + getSocket().close(); + } + catch (IOException ex) + { + } + + // Wait for the main server thread to close + + if (m_srvThread != null) + { + + try + { + m_srvThread.join(3000); + } + catch (Exception ex) + { + } + } + + // Fire a shutdown notification event + + fireServerEvent(ServerListener.ServerShutdown); + } + + /** + * Start the FTP server in a seperate thread + */ + public void startServer() + { + + // Create a seperate thread to run the FTP server + + m_srvThread = new Thread(this); + m_srvThread.setName("FTP Server"); + m_srvThread.start(); + + // Fire a server startup event + + fireServerEvent(ServerListener.ServerStartup); + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPPath.java b/source/java/org/alfresco/filesys/ftp/FTPPath.java new file mode 100644 index 0000000000..e30c7a55af --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPPath.java @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +import org.alfresco.filesys.server.filesys.*; +import org.alfresco.filesys.server.core.*; + +/** + * FTP Path Class + *

    + * Converts FTP paths to share/share relative paths. + * + * @author GKSpencer + */ +public class FTPPath +{ + + // FTP directory seperator + + private static final String FTP_SEPERATOR = "/"; + private static final char FTP_SEPERATOR_CHAR = '/'; + + // Share relative path directory seperator + + private static final String DIR_SEPERATOR = "\\"; + private static final char DIR_SEPERATOR_CHAR = '\\'; + + // FTP path + + private String m_ftpPath; + + // Share name nad share relative path + + private String m_shareName; + private String m_sharePath; + + // Shared device + + private DiskSharedDevice m_shareDev; + + // Flag to indicate if this is a directory or file path + + private boolean m_dir = true; + + /** + * Default constructor + */ + public FTPPath() + { + try + { + setFTPPath(null); + } + catch (Exception ex) + { + } + } + + /** + * Class constructor + * + * @param ftpPath String + * @exception InvalidPathException + */ + public FTPPath(String ftpPath) throws InvalidPathException + { + setFTPPath(ftpPath); + } + + /** + * Class constructor + * + * @param shrName String + * @param shrPath String + * @exception InvalidPathException + */ + public FTPPath(String shrName, String shrPath) throws InvalidPathException + { + setSharePath(shrName, shrPath); + } + + /** + * Copy constructor + * + * @param ftpPath FTPPath + */ + public FTPPath(FTPPath ftpPath) + { + try + { + setFTPPath(ftpPath.getFTPPath()); + m_shareDev = ftpPath.getSharedDevice(); + } + catch (Exception ex) + { + } + } + + /** + * Determine if the current FTP path is the root path + * + * @return boolean + */ + public final boolean isRootPath() + { + return m_ftpPath.compareTo(FTP_SEPERATOR) == 0 ? true : false; + } + + /** + * Determine if the path is for a directory or file + * + * @return boolean + */ + public final boolean isDirectory() + { + return m_dir; + } + + /** + * Check if the FTP path is valid + * + * @return boolean + */ + public final boolean hasFTPPath() + { + return m_ftpPath != null ? true : false; + } + + /** + * Return the FTP path + * + * @return String + */ + public final String getFTPPath() + { + return m_ftpPath; + } + + /** + * Check if the share name is valid + * + * @return boolean + */ + public final boolean hasShareName() + { + return m_shareName != null ? true : false; + } + + /** + * Return the share name + * + * @return String + */ + public final String getShareName() + { + return m_shareName; + } + + /** + * Check if the share path is the root path + * + * @return boolean + */ + public final boolean isRootSharePath() + { + if (m_sharePath == null || m_sharePath.compareTo(DIR_SEPERATOR) == 0) + return true; + return false; + } + + /** + * Check if the share path is valid + * + * @reutrn boolean + */ + public final boolean hasSharePath() + { + return m_sharePath != null ? true : false; + } + + /** + * Return the share relative path + * + * @reutrn String + */ + public final String getSharePath() + { + return m_sharePath; + } + + /** + * Check if the shared device has been set + * + * @return boolean + */ + public final boolean hasSharedDevice() + { + return m_shareDev != null ? true : false; + } + + /** + * Return the shared device + * + * @return DiskSharedDevice + */ + public final DiskSharedDevice getSharedDevice() + { + return m_shareDev; + } + + /** + * Set the paths using the specified FTP path + * + * @param path String + * @exception InvalidPathException + */ + public final void setFTPPath(String path) throws InvalidPathException + { + + // Check for a null path or the root path + + if (path == null || path.length() == 0 || path.compareTo(FTP_SEPERATOR) == 0) + { + m_ftpPath = FTP_SEPERATOR; + m_shareName = null; + m_sharePath = null; + m_shareDev = null; + return; + } + + // Check if the path starts with the FTP seperator + + if (path.startsWith(FTP_SEPERATOR) == false) + throw new InvalidPathException("Invalid FTP path, should start with " + FTP_SEPERATOR); + + // Save the FTP path + + m_ftpPath = path; + + // Get the first level directory from the path, this maps to the share name + + int pos = path.indexOf(FTP_SEPERATOR, 1); + if (pos != -1) + { + m_shareName = path.substring(1, pos); + if (path.length() > pos) + m_sharePath = path.substring(pos).replace(FTP_SEPERATOR_CHAR, DIR_SEPERATOR_CHAR); + else + m_sharePath = DIR_SEPERATOR; + } + else + { + m_shareName = path.substring(1); + m_sharePath = DIR_SEPERATOR; + } + + // Check if the share has changed + + if (m_shareDev != null && m_shareName != null && m_shareDev.getName().compareTo(m_shareName) != 0) + m_shareDev = null; + } + + /** + * Set the paths using the specified share and share relative path + * + * @param shr String + * @param path String + * @exception InvalidPathException + */ + public final void setSharePath(String shr, String path) throws InvalidPathException + { + + // Save the share name and path + + m_shareName = shr; + m_sharePath = path != null ? path : DIR_SEPERATOR; + + // Build the FTP style path + + StringBuffer ftpPath = new StringBuffer(); + + ftpPath.append(FTP_SEPERATOR); + if (hasShareName()) + ftpPath.append(getShareName()); + + if (hasSharePath()) + { + + // Convert the share relative path to an FTP style path + + String ftp = getSharePath().replace(DIR_SEPERATOR_CHAR, FTP_SEPERATOR_CHAR); + ftpPath.append(ftp); + } + else + ftpPath.append(FTP_SEPERATOR); + + // Update the FTP path + + m_ftpPath = ftpPath.toString(); + } + + /** + * Set the shared device + * + * @param shareList SharedDeviceList + * @param sess FTPSrvSession + * @return boolean + */ + public final boolean setSharedDevice(SharedDeviceList shareList, FTPSrvSession sess) + { + + // Clear the current shared device + + m_shareDev = null; + + // Check if the share name is valid + + if (hasShareName() == false || shareList == null) + return false; + + // Find the required disk share + + SharedDevice shr = shareList.findShare(getShareName()); + + if (shr != null && shr instanceof DiskSharedDevice) + m_shareDev = (DiskSharedDevice) shr; + + // Return the status + + return m_shareDev != null ? true : false; + } + + /** + * Build an FTP path to the specified file + * + * @param fname String + * @return String + */ + public final String makeFTPPathToFile(String fname) + { + + // Build the FTP path to a file + + StringBuffer path = new StringBuffer(256); + path.append(m_ftpPath); + if (m_ftpPath.endsWith(FTP_SEPERATOR) == false) + path.append(FTP_SEPERATOR); + path.append(fname); + + return path.toString(); + } + + /** + * Build a share relative path to the specified file + * + * @param fname String + * @return String + */ + public final String makeSharePathToFile(String fname) + { + + // Build the share relative path to a file + + StringBuilder path = new StringBuilder(256); + path.append(m_sharePath); + if (m_sharePath.endsWith(DIR_SEPERATOR) == false) + path.append(DIR_SEPERATOR); + path.append(fname); + + return path.toString(); + } + + /** + * Add a directory to the end of the current path + * + * @param dir String + */ + public final void addDirectory(String dir) + { + + // Check if the directory has a trailing seperator + + if (dir.length() > 1 && dir.endsWith(FTP_SEPERATOR) || dir.endsWith(DIR_SEPERATOR)) + dir = dir.substring(0, dir.length() - 1); + + // Append the directory to the FTP path + + StringBuilder str = new StringBuilder(256); + str.append(m_ftpPath); + + if (m_ftpPath.endsWith(FTP_SEPERATOR) == false) + str.append(FTP_SEPERATOR); + str.append(dir); + if (m_ftpPath.endsWith(FTP_SEPERATOR) == false) + str.append(FTP_SEPERATOR); + + m_ftpPath = str.toString(); + + // Check if there are any incorrect seperators in the FTP path + + if (m_ftpPath.indexOf(DIR_SEPERATOR) != -1) + m_ftpPath = m_ftpPath.replace(FTP_SEPERATOR_CHAR, DIR_SEPERATOR_CHAR); + + // Append the directory to the share relative path + + str.setLength(0); + str.append(m_sharePath); + if (m_sharePath.endsWith(DIR_SEPERATOR) == false) + str.append(DIR_SEPERATOR); + str.append(dir); + + m_sharePath = str.toString(); + + // Check if there are any incorrect seperators in the share relative path + + if (m_sharePath.indexOf(FTP_SEPERATOR) != -1) + m_sharePath = m_sharePath.replace(FTP_SEPERATOR_CHAR, DIR_SEPERATOR_CHAR); + + // Indicate that the path is to a directory + + setDirectory(true); + } + + /** + * Add a file to the end of the current path + * + * @param file String + */ + public final void addFile(String file) + { + + // Append the file name to the FTP path + + StringBuilder str = new StringBuilder(256); + str.append(m_ftpPath); + + if (m_ftpPath.endsWith(FTP_SEPERATOR) == false) + str.append(FTP_SEPERATOR); + str.append(file); + + m_ftpPath = str.toString(); + + // Check if there are any incorrect seperators in the FTP path + + if (m_ftpPath.indexOf(DIR_SEPERATOR) != -1) + m_ftpPath = m_ftpPath.replace(FTP_SEPERATOR_CHAR, DIR_SEPERATOR_CHAR); + + // Append the file name to the share relative path + + str.setLength(0); + str.append(m_sharePath); + if (m_sharePath.endsWith(DIR_SEPERATOR) == false) + str.append(DIR_SEPERATOR); + str.append(file); + + m_sharePath = str.toString(); + + // Check if there are any incorrect seperators in the share relative path + + if (m_sharePath.indexOf(FTP_SEPERATOR) != -1) + m_sharePath = m_sharePath.replace(FTP_SEPERATOR_CHAR, DIR_SEPERATOR_CHAR); + + // Indicate that the path is to a file + + setDirectory(false); + } + + /** + * Remove the last directory from the end of the path + */ + public final void removeDirectory() + { + + // Check if the FTP path has a directory to remove + + if (m_ftpPath != null && m_ftpPath.length() > 1) + { + + // Find the last directory in the FTP path + + int pos = m_ftpPath.length() - 1; + if (m_ftpPath.endsWith(FTP_SEPERATOR)) + pos--; + + while (pos > 0 && m_ftpPath.charAt(pos) != FTP_SEPERATOR_CHAR) + pos--; + + // Set the new FTP path + + m_ftpPath = m_ftpPath.substring(0, pos); + + // Indicate that the path is to a directory + + setDirectory(true); + + // Reset the share/share path + + try + { + setFTPPath(m_ftpPath); + } + catch (InvalidPathException ex) + { + } + } + } + + /** + * Set/clear the directory path flag + * + * @param dir boolean + */ + protected final void setDirectory(boolean dir) + { + m_dir = dir; + } + + /** + * Check if an FTP path string contains multiple directories + * + * @param path String + * @return boolean + */ + public final static boolean hasMultipleDirectories(String path) + { + if (path == null) + return false; + + if (path.startsWith(FTP_SEPERATOR)) + return true; + return false; + } + + /** + * Check if the FTP path is a relative path, ie. does not start with a leading slash + * + * @param path String + * @return boolean + */ + public final static boolean isRelativePath(String path) + { + if (path == null) + return false; + return path.startsWith(FTP_SEPERATOR) ? false : true; + } + + /** + * Return the FTP path as a string + * + * @return String + */ + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("["); + str.append(getFTPPath()); + str.append("="); + str.append(getShareName()); + str.append(","); + str.append(getSharePath()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPRequest.java b/source/java/org/alfresco/filesys/ftp/FTPRequest.java new file mode 100644 index 0000000000..0b8112bcd1 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPRequest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +/** + * FTP Request Class + *

    + * Contains the details of an FTP request + * + * @author GKSpencer + */ +public class FTPRequest +{ + + // FTP command id + + private int m_cmd; + + // Command argument + + private String m_arg; + + /** + * Default constructor + */ + public FTPRequest() + { + m_cmd = FTPCommand.InvalidCmd; + } + + /** + * Class constructor + * + * @param cmd int + * @param arg String + */ + public FTPRequest(int cmd, String arg) + { + m_cmd = cmd; + m_arg = arg; + } + + /** + * Class constructor + * + * @param cmdLine String + */ + public FTPRequest(String cmdLine) + { + + // Parse the FTP command record + + parseCommandLine(cmdLine); + } + + /** + * Return the command index + * + * @return int + */ + public final int isCommand() + { + return m_cmd; + } + + /** + * Check if the request has an argument + * + * @return boolean + */ + public final boolean hasArgument() + { + return m_arg != null ? true : false; + } + + /** + * Return the request argument + * + * @return String + */ + public final String getArgument() + { + return m_arg; + } + + /** + * Set the command line for the request + * + * @param cmdLine String + * @return int + */ + public final int setCommandLine(String cmdLine) + { + + // Reset the current values + + m_cmd = FTPCommand.InvalidCmd; + m_arg = null; + + // Parse the new command line + + parseCommandLine(cmdLine); + return isCommand(); + } + + /** + * Parse a command string + * + * @param cmdLine String + */ + protected final void parseCommandLine(String cmdLine) + { + + // Check if the command has an argument + + int pos = cmdLine.indexOf(' '); + String cmd = null; + + if (pos != -1) + { + cmd = cmdLine.substring(0, pos); + m_arg = cmdLine.substring(pos + 1); + } + else + cmd = cmdLine; + + // Validate the FTP command + + m_cmd = FTPCommand.getCommandId(cmd); + } + + /** + * Update the command argument + * + * @param arg String + */ + protected final void updateArgument(String arg) + { + m_arg = arg; + } + + /** + * Return the request as a string + * + * @return String + */ + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("["); + str.append(FTPCommand.getCommandName(m_cmd)); + str.append(":"); + str.append(m_arg); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPSessionList.java b/source/java/org/alfresco/filesys/ftp/FTPSessionList.java new file mode 100644 index 0000000000..622ea8da79 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPSessionList.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +import java.util.*; + +/** + * FTP Server Session List Class + * + * @author GKSpencer + */ +public class FTPSessionList +{ + + // Session list + + private Hashtable m_sessions; + + /** + * Class constructor + */ + public FTPSessionList() + { + m_sessions = new Hashtable(); + } + + /** + * Return the number of sessions in the list + * + * @return int + */ + public final int numberOfSessions() + { + return m_sessions.size(); + } + + /** + * Add a session to the list + * + * @param sess FTPSrvSession + */ + public final void addSession(FTPSrvSession sess) + { + m_sessions.put(new Integer(sess.getSessionId()), sess); + } + + /** + * Find the session using the unique session id + * + * @param id int + * @return FTPSrvSession + */ + public final FTPSrvSession findSession(int id) + { + return findSession(new Integer(id)); + } + + /** + * Find the session using the unique session id + * + * @param id Integer + * @return FTPSrvSession + */ + public final FTPSrvSession findSession(Integer id) + { + return m_sessions.get(id); + } + + /** + * Remove a session from the list + * + * @param id int + * @return FTPSrvSession + */ + public final FTPSrvSession removeSession(int id) + { + return removeSession(new Integer(id)); + } + + /** + * Remove a session from the list + * + * @param sess FTPSrvSession + * @return FTPSrvSession + */ + public final FTPSrvSession removeSession(FTPSrvSession sess) + { + return removeSession(sess.getSessionId()); + } + + /** + * Remove a session from the list + * + * @param id Integer + * @return FTPSrvSession + */ + public final FTPSrvSession removeSession(Integer id) + { + + // Find the required session + + FTPSrvSession sess = findSession(id); + + // Remove the session and return the removed session + + m_sessions.remove(id); + return sess; + } + + /** + * Enumerate the session ids + * + * @return Enumeration + */ + public final Enumeration enumerate() + { + return m_sessions.keys(); + } +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java new file mode 100644 index 0000000000..3e7682c171 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java @@ -0,0 +1,3420 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Date; +import java.util.Enumeration; +import java.util.StringTokenizer; +import java.util.Vector; + +import javax.transaction.UserTransaction; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.auth.acl.AccessControl; +import org.alfresco.filesys.server.auth.acl.AccessControlManager; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.alfresco.filesys.server.filesys.AccessMode; +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskFullException; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.DiskSharedDevice; +import org.alfresco.filesys.server.filesys.FileAction; +import org.alfresco.filesys.server.filesys.FileAttribute; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.FileStatus; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.NotifyChange; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.server.filesys.TreeConnectionHash; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * FTP Server Session Class + * + * @author GKSpencer + */ +public class FTPSrvSession extends SrvSession implements Runnable +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.ftp.protocol"); + + // Constants + // + // Debug flag values + + public static final int DBG_STATE = 0x00000001; // Session state changes + + public static final int DBG_SEARCH = 0x00000002; // File/directory search + + public static final int DBG_INFO = 0x00000004; // Information requests + + public static final int DBG_FILE = 0x00000008; // File open/close/info + + public static final int DBG_FILEIO = 0x00000010; // File read/write + + public static final int DBG_ERROR = 0x00000020; // Errors + + public static final int DBG_PKTTYPE = 0x00000040; // Received packet type + + public static final int DBG_TIMING = 0x00000080; // Time packet + + // processing + + public static final int DBG_DATAPORT = 0x00000100; // Data port + + public static final int DBG_DIRECTORY = 0x00000200; // Directory commands + + // Anonymous user name + + private static final String USER_ANONYMOUS = "anonymous"; + + // Root directory and FTP directory seperator + + private static final String ROOT_DIRECTORY = "/"; + + private static final String FTP_SEPERATOR = "/"; + + private static final char FTP_SEPERATOR_CHAR = '/'; + + // Share relative path directory seperator + + private static final String DIR_SEPERATOR = "\\"; + + private static final char DIR_SEPERATOR_CHAR = '\\'; + + // File transfer buffer size + + private static final int DEFAULT_BUFFERSIZE = 64000; + + // Carriage return/line feed combination required for response messages + + protected final static String CRLF = "\r\n"; + + // LIST command options + + protected final static String LIST_OPTION_HIDDEN = "-a"; + + // Session socket + + private Socket m_sock; + + // Input/output streams to remote client + + private InputStreamReader m_in; + + private char[] m_inbuf; + + private OutputStreamWriter m_out; + + private StringBuffer m_outbuf; + + // Data connection + + private FTPDataSession m_dataSess; + + // Current working directory details + // + // First level is the share name then a path relative to the share root + + private FTPPath m_cwd; + + // Binary mode flag + + private boolean m_binary = false; + + // Restart position for binary file transfer + + private long m_restartPos = 0; + + // Rename from path details + + private FTPPath m_renameFrom; + + // Filtered list of shared filesystems available to this session + + private SharedDeviceList m_shares; + + // List of shared device connections used by this session + + private TreeConnectionHash m_connections; + + /** + * Class constructor + * + * @param sock + * Socket + * @param srv + * FTPServer + */ + public FTPSrvSession(Socket sock, FTPNetworkServer srv) + { + super(-1, srv, "FTP", null); + + // Save the local socket + + m_sock = sock; + + // Set the socket linger options, so the socket closes immediately when + // closed + + try + { + m_sock.setSoLinger(false, 0); + } + catch (SocketException ex) + { + } + + // Indicate that the user is not logged in + + setLoggedOn(false); + + // Allocate the FTP path + + m_cwd = new FTPPath(); + + // Allocate the tree connection cache + + m_connections = new TreeConnectionHash(); + } + + /** + * Close the FTP session, and associated data socket if active + */ + public final void closeSession() + { + + // Call the base class + + super.closeSession(); + + // Close the data connection, if active + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + + // Close the socket first, if the client is still connected this should + // allow the + // input/output streams + // to be closed + + if (m_sock != null) + { + try + { + m_sock.close(); + } + catch (Exception ex) + { + } + m_sock = null; + } + + // Close the input/output streams + + if (m_in != null) + { + try + { + m_in.close(); + } + catch (Exception ex) + { + } + m_in = null; + } + + if (m_out != null) + { + try + { + m_out.close(); + } + catch (Exception ex) + { + } + m_out = null; + } + + // Remove session from server session list + + getFTPServer().removeSession(this); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("Session closed, " + getSessionId()); + } + + /** + * Return the current working directory + * + * @return String + */ + public final String getCurrentWorkingDirectory() + { + return m_cwd.getFTPPath(); + } + + /** + * Return the server that this session is associated with. + * + * @return FTPServer + */ + public final FTPNetworkServer getFTPServer() + { + return (FTPNetworkServer) getServer(); + } + + /** + * Return the client network address + * + * @return InetAddress + */ + public final InetAddress getRemoteAddress() + { + return m_sock.getInetAddress(); + } + + /** + * Check if there is a current working directory + * + * @return boolean + */ + public final boolean hasCurrentWorkingDirectory() + { + return m_cwd != null ? true : false; + } + + /** + * Set the default path for the session + * + * @param rootPath + * FTPPath + */ + public final void setRootPath(FTPPath rootPath) + { + + // Initialize the current working directory using the root path + + m_cwd = new FTPPath(rootPath); + m_cwd.setSharedDevice(getShareList(), this); + } + + /** + * Get the path details for the current request + * + * @param req + * FTPRequest + * @param filePath + * boolean + * @return FTPPath + */ + protected final FTPPath generatePathForRequest(FTPRequest req, boolean filePath) + { + return generatePathForRequest(req, filePath, true); + } + + /** + * Get the path details for the current request + * + * @param req + * FTPRequest + * @param filePath + * boolean + * @param checkExists + * boolean + * @return FTPPath + */ + protected final FTPPath generatePathForRequest(FTPRequest req, boolean filePath, boolean checkExists) + { + + // Convert the path to an FTP format path + + String path = convertToFTPSeperators(req.getArgument()); + + // Check if the path is the root directory and there is a default root + // path configured + + FTPPath ftpPath = null; + + if (path.compareTo(ROOT_DIRECTORY) == 0) + { + + // Check if the FTP server has a default root directory configured + + FTPNetworkServer ftpSrv = (FTPNetworkServer) getServer(); + if (ftpSrv.hasRootPath()) + ftpPath = ftpSrv.getRootPath(); + else + { + try + { + ftpPath = new FTPPath("/"); + } + catch (Exception ex) + { + } + return ftpPath; + } + } + + // Check if the path is relative + + else if (FTPPath.isRelativePath(path) == false) + { + + // Create a new path for the directory + + try + { + ftpPath = new FTPPath(path); + } + catch (InvalidPathException ex) + { + return null; + } + + // Find the associated shared device + + if (ftpPath.setSharedDevice(getShareList(), this) == false) + return null; + } + else + { + + // Check for the special '.' directory, just return the current + // working directory + + if (path.equals(".")) + return m_cwd; + + // Check for the special '..' directory, if already at the root + // directory return an + // error + + if (path.equals("..")) + { + + // Check if we are already at the root path + + if (m_cwd.isRootPath() == false) + { + + // Remove the last directory from the path + + m_cwd.removeDirectory(); + m_cwd.setSharedDevice(getShareList(), this); + return m_cwd; + } + else + return null; + } + + // Create a copy of the current working directory and append the new + // file/directory name + + ftpPath = new FTPPath(m_cwd); + + // Check if the root directory/share has been set + + if (ftpPath.isRootPath()) + { + + // Path specifies the share name + + try + { + ftpPath.setSharePath(path, null); + } + catch (InvalidPathException ex) + { + return null; + } + } + else + { + if (filePath) + ftpPath.addFile(path); + else + ftpPath.addDirectory(path); + } + + // Find the associated shared device, if not already set + + if (ftpPath.hasSharedDevice() == false && ftpPath.setSharedDevice(getShareList(), this) == false) + return null; + } + + // Check if the generated path exists + + if (checkExists) + { + + // Check if the new path exists and is a directory + + DiskInterface disk = null; + TreeConnection tree = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Access the virtual filesystem driver + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + + // Check if the path exists + + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.NotExist) + { + + // Get the path string, check if there is a leading + // seperator + + String pathStr = req.getArgument(); + if (pathStr.startsWith(FTP_SEPERATOR) == false) + pathStr = FTP_SEPERATOR + pathStr; + + // Create the root path + + ftpPath = new FTPPath(pathStr); + + // Find the associated shared device + + if (ftpPath.setSharedDevice(getShareList(), this) == false) + ftpPath = null; + else + { + // Recheck if the path exists + + sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + if ( sts == FileStatus.NotExist) + ftpPath = null; + } + } + else if ((sts == FileStatus.FileExists && filePath == false) + || (sts == FileStatus.DirectoryExists && filePath == true)) + { + + // Path exists but is the wrong type (directory or file) + + ftpPath = null; + } + } + catch (Exception ex) + { + ftpPath = null; + } + } + + // Return the new path + + return ftpPath; + } + + /** + * Convert a path string from share path seperators to FTP path seperators + * + * @param path + * String + * @return String + */ + protected final String convertToFTPSeperators(String path) + { + + // Check if the path is valid + + if (path == null || path.indexOf(DIR_SEPERATOR) == -1) + return path; + + // Replace the path seperators + + return path.replace(DIR_SEPERATOR_CHAR, FTP_SEPERATOR_CHAR); + } + + /** + * Find the required disk shared device + * + * @param name + * String + * @return DiskSharedDevice + */ + protected final DiskSharedDevice findShare(String name) + { + + // Check if the name is valid + + if (name == null) + return null; + + // Find the required disk share + + SharedDevice shr = getFTPServer().getShareList().findShare(m_cwd.getShareName()); + + if (shr != null && shr instanceof DiskSharedDevice) + return (DiskSharedDevice) shr; + + // Disk share not found + + return null; + } + + /** + * Set the binary mode flag + * + * @param bin + * boolean + */ + protected final void setBinary(boolean bin) + { + m_binary = bin; + } + + /** + * Send an FTP command response + * + * @param stsCode + * int + * @param msg + * String + * @exception IOException + */ + protected final void sendFTPResponse(int stsCode, String msg) throws IOException + { + + // Build the output record + + m_outbuf.setLength(0); + m_outbuf.append(stsCode); + m_outbuf.append(" "); + + if (msg != null) + m_outbuf.append(msg); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR) && stsCode >= 500) + logger.debug("Error status=" + stsCode + ", msg=" + msg); + + // Add the CR/LF + + m_outbuf.append(CRLF); + + // Output the FTP response + + if (m_out != null) + { + m_out.write(m_outbuf.toString()); + m_out.flush(); + } + } + + /** + * Send an FTP command response + * + * @param msg + * StringBuffer + * @exception IOException + */ + protected final void sendFTPResponse(StringBuffer msg) throws IOException + { + + // Output the FTP response + + if (m_out != null) + { + m_out.write(msg.toString()); + m_out.write(CRLF); + m_out.flush(); + } + } + + /** + * Process a user command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procUser(FTPRequest req) throws IOException + { + + // Clear the current client information + + setClientInformation(null); + setLoggedOn(false); + + // Check if a user name has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error in parameters or arguments"); + return; + } + + // Check for an anonymous login + + if (getFTPServer().allowAnonymous() == true + && req.getArgument().equalsIgnoreCase(getFTPServer().getAnonymousAccount())) + { + + // Anonymous login, create guest client information + + ClientInfo cinfo = new ClientInfo(getFTPServer().getAnonymousAccount(), null); + cinfo.setGuest(true); + setClientInformation(cinfo); + + // Return the anonymous login response + + sendFTPResponse(331, "Guest login ok, send your complete e-mail address as password"); + return; + } + + // Create client information for the user + + setClientInformation(new ClientInfo(req.getArgument(), null)); + + // Valid user, wait for the password + + sendFTPResponse(331, "User name okay, need password for " + req.getArgument()); + } + + /** + * Process a password command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procPassword(FTPRequest req) throws IOException + { + + // Check if the client information has been set, this indicates a user + // command has been + // received + + if (hasClientInformation() == false) + { + sendFTPResponse(500, "Syntax error, command " + + FTPCommand.getCommandName(req.isCommand()) + " unrecognized"); + return; + } + + // Check for an anonymous login, accept any password string + + if (getClientInformation().isGuest()) + { + + // Save the anonymous login password string + + getClientInformation().setPassword(req.getArgument()); + + // Accept the login + + setLoggedOn(true); + sendFTPResponse(230, "User logged in, proceed"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("Anonymous login, info=" + req.getArgument()); + } + + // Validate the user + + else + { + + // Get the client information and store the received plain text + // password + + getClientInformation().setPassword(req.getArgument()); + + // Authenticate the user + + SrvAuthenticator auth = getServer().getConfiguration().getAuthenticator(); + + int access = auth.authenticateUserPlainText(getClientInformation(), this); + + if (access == SrvAuthenticator.AUTH_ALLOW) + { + + // User successfully logged on + + sendFTPResponse(230, "User logged in, proceed"); + setLoggedOn(true); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("User " + getClientInformation().getUserName() + ", logon successful"); + } + else + { + + // Return an access denied error + + sendFTPResponse(530, "Access denied"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("User " + getClientInformation().getUserName() + ", logon failed"); + + // Close the connection + + closeSession(); + } + } + + // If the user has successfully logged on to the FTP server then inform + // listeners + + if (isLoggedOn()) + getFTPServer().sessionLoggedOn(this); + } + + /** + * Process a port command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procPort(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if the parameter has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Required argument missing"); + return; + } + + // Parse the address/port string into a IP address and port + + StringTokenizer token = new StringTokenizer(req.getArgument(), ","); + if (token.countTokens() != 6) + { + sendFTPResponse(501, "Invalid argument"); + return; + } + + // Parse the client address + + String addrStr = token.nextToken() + + "." + token.nextToken() + "." + token.nextToken() + "." + token.nextToken(); + InetAddress addr = null; + + try + { + addr = InetAddress.getByName(addrStr); + } + catch (UnknownHostException ex) + { + sendFTPResponse(501, "Invalid argument (address)"); + return; + } + + // Parse the client port + + int port = -1; + + try + { + port = Integer.parseInt(token.nextToken()) * 256; + port += Integer.parseInt(token.nextToken()); + } + catch (NumberFormatException ex) + { + sendFTPResponse(501, "Invalid argument (port)"); + return; + } + + // Create an active data session, the actual socket connection will be + // made later + + m_dataSess = getFTPServer().allocateDataSession(this, addr, port); + + // Return a success response to the client + + sendFTPResponse(200, "Port OK"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DATAPORT)) + logger.debug("Port open addr=" + addr + ", port=" + port); + } + + /** + * Process a passive command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procPassive(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Create a passive data session + + try + { + m_dataSess = getFTPServer().allocateDataSession(this, null, 0); + } + catch (IOException ex) + { + m_dataSess = null; + } + + // Check if the data session is valid + + if (m_dataSess == null) + { + sendFTPResponse(550, "Requested action not taken"); + return; + } + + // Get the passive connection address/port and return to the client + + int pasvPort = m_dataSess.getPassivePort(); + + StringBuffer msg = new StringBuffer(); + + msg.append("227 Entering Passive Mode ("); + msg.append(getFTPServer().getLocalFTPAddressString()); + msg.append(","); + msg.append(pasvPort >> 8); + msg.append(","); + msg.append(pasvPort & 0xFF); + msg.append(")"); + + sendFTPResponse(msg); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DATAPORT)) + logger.debug("Passive open addr=" + getFTPServer().getLocalFTPAddressString() + ", port=" + pasvPort); + } + + /** + * Process a print working directory command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procPrintWorkDir(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Return the current working directory virtual path + + sendFTPResponse(257, "\"" + m_cwd.getFTPPath() + "\""); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DIRECTORY)) + logger.debug("Pwd ftp=" + + m_cwd.getFTPPath() + ", share=" + m_cwd.getShareName() + ", path=" + m_cwd.getSharePath()); + } + + /** + * Process a change working directory command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procChangeWorkDir(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if the request has a valid argument + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Path not specified"); + return; + } + + // Create the new working directory path + + FTPPath newPath = generatePathForRequest(req, false); + if (newPath == null) + { + sendFTPResponse(550, "Invalid path " + req.getArgument()); + return; + } + + // Set the new current working directory + + m_cwd = newPath; + + // Return a success status + + sendFTPResponse(250, "Requested file action OK"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DIRECTORY)) + logger.debug("Cwd ftp=" + + m_cwd.getFTPPath() + ", share=" + m_cwd.getShareName() + ", path=" + m_cwd.getSharePath()); + } + + /** + * Process a change directory up command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procCdup(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if there is a current working directory path + + if (m_cwd.isRootPath()) + { + + // Already at the root directory, return an error status + + sendFTPResponse(550, "Already at root directory"); + return; + } + else + { + + // Remove the last directory from the path + + m_cwd.removeDirectory(); + if (m_cwd.isRootPath() == false && m_cwd.getSharedDevice() == null) + m_cwd.setSharedDevice(getShareList(), this); + } + + // Return a success status + + sendFTPResponse(250, "Requested file action OK"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DIRECTORY)) + logger.debug("Cdup ftp=" + + m_cwd.getFTPPath() + ", share=" + m_cwd.getShareName() + ", path=" + m_cwd.getSharePath()); + } + + /** + * Process a long directory listing command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procList(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if the client has requested hidden files, via the '-a' option + + boolean hidden = false; + + if (req.hasArgument() && req.getArgument().startsWith(LIST_OPTION_HIDDEN)) + { + // Indicate that we want hidden files in the listing + + hidden = true; + + // Remove the option from the command argument, and update the + // request + + String arg = req.getArgument(); + int pos = arg.indexOf(" "); + if (pos > 0) + arg = arg.substring(pos + 1); + else + arg = null; + + req.updateArgument(arg); + } + + // Create the path for the file listing + + FTPPath ftpPath = m_cwd; + if (req.hasArgument()) + ftpPath = generatePathForRequest(req, true); + + if (ftpPath == null) + { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Check if the session has the required access + + if (ftpPath.isRootPath() == false) + { + + // Check if the session has access to the filesystem + + TreeConnection tree = getTreeConnection(ftpPath.getSharedDevice()); + if (tree == null || tree.hasReadAccess() == false) + { + + // Session does not have access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + } + + // Send the intermediate response + + sendFTPResponse(150, "File status okay, about to open data connection"); + + // Check if there is an active data session + + if (m_dataSess == null) + { + sendFTPResponse(425, "Can't open data connection"); + return; + } + + // Get the data connection socket + + Socket dataSock = null; + + try + { + dataSock = m_dataSess.getSocket(); + } + catch (Exception ex) + { + logger.debug(ex); + } + + if (dataSock == null) + { + sendFTPResponse(426, "Connection closed; transfer aborted"); + return; + } + + // Output the directory listing to the client + + Writer dataWrt = null; + + try + { + + // Open an output stream to the client + + dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); + + // Check if a path has been specified to list + + Vector files = null; + + if (req.hasArgument()) + { + } + + // Get a list of file information objects for the current directory + + files = listFilesForPath(ftpPath, false, hidden); + + // Output the file list to the client + + if (files != null) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_SEARCH)) + logger.debug("List found " + files.size() + " files in " + ftpPath.getFTPPath()); + + // Output the file information to the client + + StringBuffer str = new StringBuffer(256); + + for (FileInfo finfo : files) + { + + // Build the output record + + str.setLength(0); + + str.append(finfo.isDirectory() ? "d" : "-"); + str.append("rw-rw-rw- 1 user group "); + str.append(finfo.getSize()); + str.append(" "); + + FTPDate.packUnixDate(str, new Date(finfo.getModifyDateTime())); + + str.append(" "); + str.append(finfo.getFileName()); + str.append(CRLF); + + // Output the file information record + + dataWrt.write(str.toString()); + } + + // Flush the data stream + + dataWrt.flush(); + } + + // Close the data stream and socket + + dataWrt.close(); + dataWrt = null; + + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + + // End of file list transmission + + sendFTPResponse(226, "Closing data connection"); + } + catch (Exception ex) + { + + // Failed to send file listing + + sendFTPResponse(451, "Error reading file list"); + } finally + { + + // Close the data stream to the client + + if (dataWrt != null) + dataWrt.close(); + + // Close the data connection to the client + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + } + } + + /** + * Process a short directory listing command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procNList(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Create the path for the file listing + + FTPPath ftpPath = m_cwd; + if (req.hasArgument()) + ftpPath = generatePathForRequest(req, true); + + if (ftpPath == null) + { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Check if the session has the required access + + if (ftpPath.isRootPath() == false) + { + + // Check if the session has access to the filesystem + + TreeConnection tree = getTreeConnection(ftpPath.getSharedDevice()); + if (tree == null || tree.hasReadAccess() == false) + { + + // Session does not have access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + } + + // Send the intermediate response + + sendFTPResponse(150, "File status okay, about to open data connection"); + + // Check if there is an active data session + + if (m_dataSess == null) + { + sendFTPResponse(425, "Can't open data connection"); + return; + } + + // Get the data connection socket + + Socket dataSock = null; + + try + { + dataSock = m_dataSess.getSocket(); + } + catch (Exception ex) + { + logger.error("Data socket error", ex); + } + + if (dataSock == null) + { + sendFTPResponse(426, "Connection closed; transfer aborted"); + return; + } + + // Output the directory listing to the client + + Writer dataWrt = null; + + try + { + + // Open an output stream to the client + + dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); + + // Check if a path has been specified to list + + Vector files = null; + + if (req.hasArgument()) + { + } + + // Get a list of file information objects for the current directory + + files = listFilesForPath(ftpPath, false, false); + + // Output the file list to the client + + if (files != null) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_SEARCH)) + logger.debug("List found " + files.size() + " files in " + ftpPath.getFTPPath()); + + // Output the file information to the client + + for (FileInfo finfo : files) + { + + // Output the file information record + + dataWrt.write(finfo.getFileName()); + dataWrt.write(CRLF); + } + } + + // End of file list transmission + + sendFTPResponse(226, "Closing data connection"); + } + catch (Exception ex) + { + + // Failed to send file listing + + sendFTPResponse(451, "Error reading file list"); + } finally + { + + // Close the data stream to the client + + if (dataWrt != null) + dataWrt.close(); + + // Close the data connection to the client + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + } + } + + /** + * Process a system status command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procSystemStatus(FTPRequest req) throws IOException + { + + // Return the system type + + sendFTPResponse(215, "UNIX Type: Java FTP Server"); + } + + /** + * Process a server status command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procServerStatus(FTPRequest req) throws IOException + { + + // Return server status information + + sendFTPResponse(211, "JLAN Server - Java FTP Server"); + } + + /** + * Process a help command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procHelp(FTPRequest req) throws IOException + { + + // Return help information + + sendFTPResponse(211, "HELP text"); + } + + /** + * Process a no-op command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procNoop(FTPRequest req) throws IOException + { + + // Return a response + + sendFTPResponse(200, ""); + } + + /** + * Process a quit command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procQuit(FTPRequest req) throws IOException + { + + // Return a response + + sendFTPResponse(221, "Bye"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("Quit closing connection(s) to client"); + + // Close the session(s) to the client + + closeSession(); + } + + /** + * Process a type command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procType(FTPRequest req) throws IOException + { + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Check if ASCII or binary mode is enabled + + String arg = req.getArgument().toUpperCase(); + if (arg.startsWith("A")) + setBinary(false); + else if (arg.startsWith("I") || arg.startsWith("L")) + setBinary(true); + else + { + + // Invalid argument + + sendFTPResponse(501, "Syntax error, invalid parameter"); + return; + } + + // Return a success status + + sendFTPResponse(200, "Command OK"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("Type arg=" + req.getArgument() + ", binary=" + m_binary); + } + + /** + * Process a restart command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procRestart(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Validate the restart position + + try + { + m_restartPos = Integer.parseInt(req.getArgument()); + } + catch (NumberFormatException ex) + { + sendFTPResponse(501, "Invalid restart position"); + return; + } + + // Return a success status + + sendFTPResponse(350, "Restart OK"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILEIO)) + logger.debug("Restart pos=" + m_restartPos); + } + + /** + * Process a return file command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procReturnFile(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Create the path for the file listing + + FTPPath ftpPath = generatePathForRequest(req, true); + if (ftpPath == null) + { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Check if the path is the root directory + + if (ftpPath.isRootPath() || ftpPath.isRootSharePath()) + { + sendFTPResponse(550, "That is a directory"); + return; + } + + // Send the intermediate response + + sendFTPResponse(150, "Connection accepted"); + + // Check if there is an active data session + + if (m_dataSess == null) + { + sendFTPResponse(425, "Can't open data connection"); + return; + } + + // Get the data connection socket + + Socket dataSock = null; + + try + { + dataSock = m_dataSess.getSocket(); + } + catch (Exception ex) + { + } + + if (dataSock == null) + { + sendFTPResponse(426, "Connection closed; transfer aborted"); + return; + } + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("Returning ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + ftpPath.getSharePath()); + + // Send the file to the client + + OutputStream os = null; + DiskInterface disk = null; + TreeConnection tree = null; + NetworkFile netFile = null; + + try + { + + // Open an output stream to the client + + os = dataSock.getOutputStream(); + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the file exists and it is a file, if so then open the + // file + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + + // Create the file open parameters + + FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), FileAction.OpenIfExists, + AccessMode.ReadOnly, 0); + + // Check if the file exists and it is a file + + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.FileExists) + { + + // Open the file + + netFile = disk.openFile(this, tree, params); + } + + // Check if the file has been opened + + if (netFile == null) + { + sendFTPResponse(550, "File " + req.getArgument() + " not available"); + return; + } + + // Allocate the buffer for the file data + + byte[] buf = new byte[DEFAULT_BUFFERSIZE]; + long filePos = m_restartPos; + + int len = -1; + + while (filePos < netFile.getFileSize()) + { + + // Read another block of data from the file + + len = disk.readFile(this, tree, netFile, buf, 0, buf.length, filePos); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILEIO)) + logger.debug(" Write len=" + len + " bytes"); + + // Write the current data block to the client, update the file + // position + + if (len > 0) + { + + // Write the data to the client + + os.write(buf, 0, len); + + // Update the file position + + filePos += len; + } + } + + // Close the output stream to the client + + os.close(); + os = null; + + // Indicate that the file has been transmitted + + sendFTPResponse(226, "Closing data connection"); + + // Close the data session + + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + + // Close the network file + + disk.closeFile(this, tree, netFile); + netFile = null; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILEIO)) + logger.debug(" Transfer complete, file closed"); + } + catch (SocketException ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR)) + logger.debug(" Error during transfer", ex); + + // Close the data socket to the client + + if (m_dataSess != null) + { + m_dataSess.closeSession(); + m_dataSess = null; + } + + // Indicate that there was an error during transmission of the file + // data + + sendFTPResponse(426, "Data connection closed by client"); + } + catch (Exception ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR)) + logger.debug(" Error during transfer", ex); + + // Indicate that there was an error during transmission of the file + // data + + sendFTPResponse(426, "Error during transmission"); + } finally + { + + // Close the network file + + if (netFile != null && disk != null && tree != null) + disk.closeFile(this, tree, netFile); + + // Close the output stream to the client + + if (os != null) + os.close(); + + // Close the data connection to the client + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + } + } + + /** + * Process a store file command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procStoreFile(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Create the path for the file listing + + FTPPath ftpPath = generatePathForRequest(req, true, false); + if (ftpPath == null) + { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Send the file to the client + + InputStream is = null; + DiskInterface disk = null; + TreeConnection tree = null; + NetworkFile netFile = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the session has the required access to the filesystem + + if (tree == null || tree.hasWriteAccess() == false) + { + + // Session does not have write access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + + // Check if the file exists + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.DirectoryExists) + { + + // Return an error status + + sendFTPResponse(500, "Invalid path (existing directory)"); + return; + } + + // Create the file open parameters + + FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), + sts == FileStatus.FileExists ? FileAction.TruncateExisting : FileAction.CreateNotExist, + AccessMode.ReadWrite, 0); + + // Create a new file to receive the data + + if (sts == FileStatus.FileExists) + { + + // Overwrite the existing file + + netFile = disk.openFile(this, tree, params); + } + else + { + + // Create a new file + + netFile = disk.createFile(this, tree, params); + } + + // Notify change listeners that a new file has been created + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionAdded, ftpPath.getSharePath()); + + // Send the intermediate response + + sendFTPResponse(150, "File status okay, about to open data connection"); + + // Check if there is an active data session + + if (m_dataSess == null) + { + sendFTPResponse(425, "Can't open data connection"); + return; + } + + // Get the data connection socket + + Socket dataSock = null; + + try + { + dataSock = m_dataSess.getSocket(); + } + catch (Exception ex) + { + } + + if (dataSock == null) + { + sendFTPResponse(426, "Connection closed; transfer aborted"); + return; + } + + // Open an input stream from the client + + is = dataSock.getInputStream(); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("Storing ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + + ftpPath.getSharePath()); + + // Allocate the buffer for the file data + + byte[] buf = new byte[DEFAULT_BUFFERSIZE]; + long filePos = 0; + int len = is.read(buf, 0, buf.length); + + while (len > 0) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILEIO)) + logger.debug(" Receive len=" + len + " bytes"); + + // Write the current data block to the file, update the file + // position + + disk.writeFile(this, tree, netFile, buf, 0, len, filePos); + filePos += len; + + // Read another block of data from the client + + len = is.read(buf, 0, buf.length); + } + + // Close the input stream from the client + + is.close(); + is = null; + + // Close the network file + + disk.closeFile(this, tree, netFile); + netFile = null; + + // Indicate that the file has been received + + sendFTPResponse(226, "Closing data connection"); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILEIO)) + logger.debug(" Transfer complete, file closed"); + } + catch (SocketException ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR)) + logger.debug(" Error during transfer", ex); + + // Close the data socket to the client + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + + // Indicate that there was an error during transmission of the file + // data + + sendFTPResponse(426, "Data connection closed by client"); + } + catch (DiskFullException ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR)) + logger.debug(" Error during transfer", ex); + + // Close the data socket to the client + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + + // Indicate that there was an error during writing of the file + + sendFTPResponse(451, "Disk full"); + } + catch (Exception ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR)) + logger.debug(" Error during transfer", ex); + ex.printStackTrace(); + + // Indicate that there was an error during transmission of the file + // data + + sendFTPResponse(426, "Error during transmission"); + } finally + { + + // Close the network file + + if (netFile != null && disk != null && tree != null) + disk.closeFile(this, tree, netFile); + + // Close the input stream to the client + + if (is != null) + is.close(); + + // Close the data connection to the client + + if (m_dataSess != null) + { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + } + } + + /** + * Process a delete file command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procDeleteFile(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Create the path for the file + + FTPPath ftpPath = generatePathForRequest(req, true); + if (ftpPath == null) + { + sendFTPResponse(550, "Invalid path specified"); + return; + } + + // Delete the specified file + + DiskInterface disk = null; + TreeConnection tree = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the session has the required access to the filesystem + + if (tree == null || tree.hasWriteAccess() == false) + { + + // Session does not have write access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + + // Check if the file exists and it is a file + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.FileExists) + { + + // Delete the file + + disk.deleteFile(this, tree, ftpPath.getSharePath()); + + // Check if there are any file/directory change notify requests + // active + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionRemoved, ftpPath.getSharePath()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("Deleted ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + + ftpPath.getSharePath()); + } + else + { + + // File does not exist or is a directory + + sendFTPResponse(550, "File " + + req.getArgument() + (sts == FileStatus.NotExist ? " not available" : " is a directory")); + return; + } + } + catch (Exception ex) + { + sendFTPResponse(450, "File action not taken"); + return; + } + + // Return a success status + + sendFTPResponse(250, "File " + req.getArgument() + " deleted"); + } + + /** + * Process a rename from command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procRenameFrom(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Clear the current rename from path details, if any + + m_renameFrom = null; + + // Create the path for the file/directory + + FTPPath ftpPath = generatePathForRequest(req, false, false); + if (ftpPath == null) + { + sendFTPResponse(550, "Invalid path specified"); + return; + } + + // Check that the file exists, and it is a file + + DiskInterface disk = null; + TreeConnection tree = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the session has the required access to the filesystem + + if (tree == null || tree.hasWriteAccess() == false) + { + + // Session does not have write access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + + // Check if the file exists and it is a file + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts != FileStatus.NotExist) + { + + // Save the rename from file details, rename to command should + // follow + + m_renameFrom = ftpPath; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("RenameFrom ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + + ftpPath.getSharePath()); + } + else + { + + // File/directory does not exist + + sendFTPResponse(550, "File " + + req.getArgument() + (sts == FileStatus.NotExist ? " not available" : " is a directory")); + return; + } + } + catch (Exception ex) + { + sendFTPResponse(450, "File action not taken"); + return; + } + + // Return a success status + + sendFTPResponse(350, "File " + req.getArgument() + " OK"); + } + + /** + * Process a rename to command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procRenameTo(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Check if the rename from has already been set + + if (m_renameFrom == null) + { + sendFTPResponse(550, "Rename from not set"); + return; + } + + // Create the path for the new file name + + FTPPath ftpPath = generatePathForRequest(req, true, false); + if (ftpPath == null) + { + sendFTPResponse(550, "Invalid path specified"); + return; + } + + // Check that the rename is on the same share + + if (m_renameFrom.getShareName().compareTo(ftpPath.getShareName()) != 0) + { + sendFTPResponse(550, "Cannot rename across shares"); + return; + } + + // Rename the file + + DiskInterface disk = null; + TreeConnection tree = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the session has the required access to the filesystem + + if (tree == null || tree.hasWriteAccess() == false) + { + + // Session does not have write access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + + // Check if the file exists and it is a file + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.NotExist) + { + + // Rename the file/directory + + disk.renameFile(this, tree, m_renameFrom.getSharePath(), ftpPath.getSharePath()); + + // Check if there are any file/directory change notify requests + // active + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyRename(m_renameFrom.getSharePath(), ftpPath.getSharePath()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("RenameTo ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + + ftpPath.getSharePath()); + } + else + { + + // File does not exist or is a directory + + sendFTPResponse(550, "File " + + req.getArgument() + (sts == FileStatus.NotExist ? " not available" : " is a directory")); + return; + } + } + catch (Exception ex) + { + sendFTPResponse(450, "File action not taken"); + return; + } finally + { + + // Clear the rename details + + m_renameFrom = null; + } + + // Return a success status + + sendFTPResponse(250, "File renamed OK"); + } + + /** + * Process a create directory command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procCreateDirectory(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Check if the new directory contains multiple directories + + FTPPath ftpPath = generatePathForRequest(req, false, false); + if (ftpPath == null) + { + sendFTPResponse(550, "Invalid path " + req.getArgument()); + return; + } + + // Create the new directory + + DiskInterface disk = null; + TreeConnection tree = null; + NetworkFile netFile = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the session has the required access to the filesystem + + if (tree == null || tree.hasWriteAccess() == false) + { + + // Session does not have write access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + + // Check if the directory exists + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.NotExist) + { + + // Create the new directory + + FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), FileAction.CreateNotExist, + AccessMode.ReadWrite, FileAttribute.NTDirectory); + + disk.createDirectory(this, tree, params); + + // Notify change listeners that a new directory has been created + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionAdded, ftpPath.getSharePath()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DIRECTORY)) + logger.debug("CreateDir ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + + ftpPath.getSharePath()); + } + else + { + + // File/directory already exists with that name, return an error + + sendFTPResponse(450, sts == FileStatus.FileExists ? "File exists with that name" + : "Directory already exists"); + return; + } + } + catch (Exception ex) + { + sendFTPResponse(450, "Failed to create directory"); + return; + } + + // Return the FTP path to the client + + sendFTPResponse(250, ftpPath.getFTPPath()); + } + + /** + * Process a delete directory command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procRemoveDirectory(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Check if the directory path contains multiple directories + + FTPPath ftpPath = generatePathForRequest(req, false); + if (ftpPath == null) + { + sendFTPResponse(550, "Invalid path " + req.getArgument()); + return; + } + + // Check if the path is the root directory, cannot delete directories + // from the root + // directory + // as it maps to the list of available disk shares. + + if (ftpPath.isRootPath() || ftpPath.isRootSharePath()) + { + sendFTPResponse(550, "Access denied, cannot delete directory in root"); + return; + } + + // Delete the directory + + DiskInterface disk = null; + TreeConnection tree = null; + NetworkFile netFile = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Check if the session has the required access to the filesystem + + if (tree == null || tree.hasWriteAccess() == false) + { + + // Session does not have write access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + + // Check if the directory exists + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + int sts = disk.fileExists(this, tree, ftpPath.getSharePath()); + + if (sts == FileStatus.DirectoryExists) + { + + // Delete the new directory + + disk.deleteDirectory(this, tree, ftpPath.getSharePath()); + + // Check if there are any file/directory change notify requests + // active + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionRemoved, ftpPath.getSharePath()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_DIRECTORY)) + logger.debug("DeleteDir ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path=" + + ftpPath.getSharePath()); + } + else + { + + // File already exists with that name or directory does not + // exist return an error + + sendFTPResponse(550, sts == FileStatus.FileExists ? "File exists with that name" + : "Directory does not exist"); + return; + } + } + catch (Exception ex) + { + sendFTPResponse(550, "Failed to delete directory"); + return; + } + + // Return a success status + + sendFTPResponse(250, "Directory deleted OK"); + } + + /** + * Process a modify date/time command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procModifyDateTime(FTPRequest req) throws IOException + { + + // Return a success response + + sendFTPResponse(550, "Not implemented yet"); + } + + /** + * Process a file size command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procFileSize(FTPRequest req) throws IOException + { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Create the path for the file listing + + FTPPath ftpPath = generatePathForRequest(req, true); + if (ftpPath == null) + { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Get the file information + + DiskInterface disk = null; + TreeConnection tree = null; + + try + { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Access the virtual filesystem driver + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + + // Get the file information + + FileInfo finfo = disk.getFileInformation(null, tree, ftpPath.getSharePath()); + + if (finfo == null) + { + sendFTPResponse(550, "File " + req.getArgument() + " not available"); + return; + } + + // Return the file size + + sendFTPResponse(213, "" + finfo.getSize()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("File size ftp=" + + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", size=" + finfo.getSize()); + } + catch (Exception ex) + { + sendFTPResponse(550, "Error retrieving file size"); + } + } + + /** + * Process a structure command. This command is obsolete. + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procStructure(FTPRequest req) throws IOException + { + + // Check for the file structure argument + + if (req.hasArgument() && req.getArgument().equalsIgnoreCase("F")) + sendFTPResponse(200, "OK"); + + // Return an error response + + sendFTPResponse(504, "Obsolete"); + } + + /** + * Process a mode command. This command is obsolete. + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procMode(FTPRequest req) throws IOException + { + + // Check for the stream transfer mode argument + + if (req.hasArgument() && req.getArgument().equalsIgnoreCase("S")) + sendFTPResponse(200, "OK"); + + // Return an error response + + sendFTPResponse(504, "Obsolete"); + } + + /** + * Process an allocate command. This command is obsolete. + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procAllocate(FTPRequest req) throws IOException + { + + // Return a response + + sendFTPResponse(202, "Obsolete"); + } + + /** + * Build a list of file name or file information objects for the specified + * server path + * + * @param path + * FTPPath + * @param nameOnly + * boolean + * @param hidden + * boolean + * @return Vector + */ + protected final Vector listFilesForPath(FTPPath path, boolean nameOnly, boolean hidden) + { + + // Check if the path is valid + + if (path == null) + return null; + + // Check if the path is the root path + + Vector files = new Vector(); + + if (path.hasSharedDevice() == false) + { + + // The first level of directories are mapped to the available shares + + SharedDeviceList shares = getShareList(); + if (shares != null) + { + + // Search for disk shares + + Enumeration enm = shares.enumerateShares(); + + while (enm.hasMoreElements()) + { + + // Get the current shared device + + SharedDevice shr = enm.nextElement(); + + // Add the share name or full information to the list + + if (nameOnly == false) + { + + // Create a file information object for the top level + // directory details + + FileInfo finfo = new FileInfo(shr.getName(), 0L, FileAttribute.Directory); + files.add(finfo); + } + else + files.add(new FileInfo(shr.getName(), 0L, FileAttribute.Directory)); + } + } + } + else + { + + // Append a wildcard to the search path + + String searchPath = path.getSharePath(); + + if (path.isDirectory()) + searchPath = path.makeSharePathToFile("*"); + + // Create a temporary tree connection + + TreeConnection tree = new TreeConnection(path.getSharedDevice()); + + // Start a search on the specified disk share + + DiskInterface disk = null; + SearchContext ctx = null; + + int searchAttr = FileAttribute.Directory + FileAttribute.Normal; + if (hidden) + searchAttr += FileAttribute.Hidden; + + try + { + disk = (DiskInterface) path.getSharedDevice().getInterface(); + ctx = disk.startSearch(this, tree, searchPath, searchAttr); + } + catch (Exception ex) + { + } + + // Add the files to the list + + if (ctx != null) + { + + // Get the file names/information + + while (ctx.hasMoreFiles()) + { + + // Check if a file name or file information is required + + if (nameOnly) + { + + // Add a file name to the list + + files.add(new FileInfo(ctx.nextFileName(), 0L, 0)); + } + else + { + + // Create a file information object + + FileInfo finfo = new FileInfo(); + + if (ctx.nextFileInfo(finfo) == false) + break; + if (finfo.getFileName() != null) + files.add(finfo); + } + } + } + } + + // Return the list of file names/information + + return files; + } + + /** + * Get the list of filtered shares that are available to this session + * + * @return SharedDeviceList + */ + protected final SharedDeviceList getShareList() + { + + // Check if the filtered share list has been initialized + + if (m_shares == null) + { + + // Get a list of shared filesystems + + SharedDeviceList shares = getFTPServer().getShareMapper().getShareList(getFTPServer().getServerName(), + this, false); + + // Search for disk shares + + m_shares = new SharedDeviceList(); + Enumeration enm = shares.enumerateShares(); + + while (enm.hasMoreElements()) + { + + // Get the current shared device + + SharedDevice shr = (SharedDevice) enm.nextElement(); + + // Check if the share is a disk share + + if (shr instanceof DiskSharedDevice) + m_shares.addShare(shr); + } + + // Check if there is an access control manager available, if so then + // filter the list of + // shared filesystems + + if (getServer().hasAccessControlManager()) + { + + // Get the access control manager + + AccessControlManager aclMgr = getServer().getAccessControlManager(); + + // Filter the list of shared filesystems + + m_shares = aclMgr.filterShareList(this, m_shares); + } + } + + // Return the filtered shared filesystem list + + return m_shares; + } + + /** + * Get a tree connection for the specified shared device. Creates and caches + * a new tree connection if required. + * + * @param share + * SharedDevice + * @return TreeConnection + */ + protected final TreeConnection getTreeConnection(SharedDevice share) + { + + // Check if the share is valid + + if (share == null) + return null; + + // Check if there is a tree connection in the cache + + TreeConnection tree = m_connections.findConnection(share.getName()); + if (tree == null) + { + + // Create a new tree connection + + tree = new TreeConnection(share); + m_connections.addConnection(tree); + + // Set the access permission for the shared filesystem + + if (getServer().hasAccessControlManager()) + { + + // Set the access permission to the shared filesystem + + AccessControlManager aclMgr = getServer().getAccessControlManager(); + + int access = aclMgr.checkAccessControl(this, share); + if (access != AccessControl.Default) + tree.setPermission(access); + } + } + + // Return the connection + + return tree; + } + + /** + * Start the FTP session in a seperate thread + */ + public void run() + { + + try + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("FTP session started"); + + // Create the input/output streams + + m_in = new InputStreamReader(m_sock.getInputStream()); + m_out = new OutputStreamWriter(m_sock.getOutputStream()); + + m_inbuf = new char[512]; + m_outbuf = new StringBuffer(256); + + // Return the initial response + + sendFTPResponse(220, "FTP server ready"); + + // Start/end times if timing debug is enabled + + long startTime = 0L; + long endTime = 0L; + + // Create an FTP request to hold command details + + FTPRequest ftpReq = new FTPRequest(); + + // The server session loops until the NetBIOS hangup state is set. + + int rdlen = -1; + String cmd = null; + + while (m_sock != null) + { + + // Wait for a data packet + + rdlen = m_in.read(m_inbuf); + + // Check if there is no more data, the other side has dropped + // the connection + + if (rdlen == -1) + { + closeSession(); + continue; + } + + // Trim the trailing + + if (rdlen > 0) + { + while (rdlen > 0 && m_inbuf[rdlen - 1] == '\r' || m_inbuf[rdlen - 1] == '\n') + rdlen--; + } + + // Get the command string + + cmd = new String(m_inbuf, 0, rdlen); + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_TIMING)) + startTime = System.currentTimeMillis(); + + if (logger.isDebugEnabled() && hasDebug(DBG_PKTTYPE)) + logger.debug("Cmd " + ftpReq); + + // Parse the received command, and validate + + ftpReq.setCommandLine(cmd); + m_reqCount++; + + switch (ftpReq.isCommand()) + { + + // User command + + case FTPCommand.User: + procUser(ftpReq); + break; + + // Password command + + case FTPCommand.Pass: + procPassword(ftpReq); + break; + + // Quit command + + case FTPCommand.Quit: + procQuit(ftpReq); + break; + + // Type command + + case FTPCommand.Type: + procType(ftpReq); + break; + + // Port command + + case FTPCommand.Port: + procPort(ftpReq); + break; + + // Passive command + + case FTPCommand.Pasv: + procPassive(ftpReq); + break; + + // Restart position command + + case FTPCommand.Rest: + procRestart(ftpReq); + break; + + // Return file command + + case FTPCommand.Retr: + procReturnFile(ftpReq); + + // Reset the restart position + + m_restartPos = 0; + break; + + // Store file command + + case FTPCommand.Stor: + procStoreFile(ftpReq); + break; + + // Print working directory command + + case FTPCommand.Pwd: + case FTPCommand.XPwd: + procPrintWorkDir(ftpReq); + break; + + // Change working directory command + + case FTPCommand.Cwd: + case FTPCommand.XCwd: + procChangeWorkDir(ftpReq); + break; + + // Change to previous directory command + + case FTPCommand.Cdup: + case FTPCommand.XCup: + procCdup(ftpReq); + break; + + // Full directory listing command + + case FTPCommand.List: + procList(ftpReq); + break; + + // Short directory listing command + + case FTPCommand.Nlst: + procNList(ftpReq); + break; + + // Delete file command + + case FTPCommand.Dele: + procDeleteFile(ftpReq); + break; + + // Rename file from command + + case FTPCommand.Rnfr: + procRenameFrom(ftpReq); + break; + + // Rename file to comand + + case FTPCommand.Rnto: + procRenameTo(ftpReq); + break; + + // Create new directory command + + case FTPCommand.Mkd: + case FTPCommand.XMkd: + procCreateDirectory(ftpReq); + break; + + // Delete directory command + + case FTPCommand.Rmd: + case FTPCommand.XRmd: + procRemoveDirectory(ftpReq); + break; + + // Return file size command + + case FTPCommand.Size: + procFileSize(ftpReq); + break; + + // Set modify date/time command + + case FTPCommand.Mdtm: + procModifyDateTime(ftpReq); + break; + + // System status command + + case FTPCommand.Syst: + procSystemStatus(ftpReq); + break; + + // Server status command + + case FTPCommand.Stat: + procServerStatus(ftpReq); + break; + + // Help command + + case FTPCommand.Help: + procHelp(ftpReq); + break; + + // No-op command + + case FTPCommand.Noop: + procNoop(ftpReq); + break; + + // Structure command (obsolete) + + case FTPCommand.Stru: + procStructure(ftpReq); + break; + + // Mode command (obsolete) + + case FTPCommand.Mode: + procMode(ftpReq); + break; + + // Allocate command (obsolete) + + case FTPCommand.Allo: + procAllocate(ftpReq); + break; + + // Unknown/unimplemented command + + default: + if (ftpReq.isCommand() != FTPCommand.InvalidCmd) + sendFTPResponse(502, "Command " + + FTPCommand.getCommandName(ftpReq.isCommand()) + " not implemented"); + else + sendFTPResponse(502, "Command not implemented"); + break; + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_TIMING)) + { + endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + if (duration > 20) + logger.debug("Processed cmd " + + FTPCommand.getCommandName(ftpReq.isCommand()) + " in " + duration + "ms"); + } + + // Check for an active transaction, and commit it + + if ( hasUserTransaction()) + { + try + { + // Commit the transaction + + UserTransaction trans = getUserTransaction(); + trans.commit(); + } + catch ( Exception ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Error committing transaction", ex); + } + } + + } // end while state + } + catch (SocketException ex) + { + + // DEBUG + + if (logger.isErrorEnabled() && hasDebug(DBG_STATE)) + logger.error("Socket closed by remote client"); + } + catch (Exception ex) + { + + // Output the exception details + + if (isShutdown() == false) + { + logger.debug(ex); + } + } + finally + { + // If there is an active transaction then roll it back + + if ( hasUserTransaction()) + { + try + { + getUserTransaction().rollback(); + } + catch (Exception ex) + { + logger.warn("Failed to rollback transaction", ex); + } + } + } + + // Cleanup the session, make sure all resources are released + + closeSession(); + + // Debug + + if (hasDebug(DBG_STATE)) + logger.debug("Server session closed"); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/ftp/InvalidPathException.java b/source/java/org/alfresco/filesys/ftp/InvalidPathException.java new file mode 100644 index 0000000000..b5b606efe3 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/InvalidPathException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.ftp; + +/** + * Invalid FTP Path Exception Class + * + * @author GKSpencer + */ +public class InvalidPathException extends Exception { + + private static final long serialVersionUID = -3298943582077910226L; + + /** + * Default constructor + */ + public InvalidPathException() { + super(); + } + + /** + * Class constructor + * + * @param msg String + */ + public InvalidPathException(String msg) { + super(msg); + } +} diff --git a/source/java/org/alfresco/filesys/locking/FileLock.java b/source/java/org/alfresco/filesys/locking/FileLock.java new file mode 100644 index 0000000000..bd86d9e599 --- /dev/null +++ b/source/java/org/alfresco/filesys/locking/FileLock.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.locking; + +/** + * File Lock Class + *

    + * Contains the details of a single file lock. + */ +public class FileLock +{ + + // Constants + + public static final long LockWholeFile = 0xFFFFFFFFFFFFFFFFL; + + // Start lock offset and length + + private long m_offset; + private long m_length; + + // Owner process id + + private int m_pid; + + /** + * Class constructor + * + * @param offset long + * @param len long + * @param pid int + */ + public FileLock(long offset, long len, int pid) + { + setOffset(offset); + setLength(len); + setProcessId(pid); + } + + /** + * Get the starting lock offset + * + * @return long Starting lock offset. + */ + public final long getOffset() + { + return m_offset; + } + + /** + * Set the starting lock offset. + * + * @param long Starting lock offset + */ + public final void setOffset(long offset) + { + m_offset = offset; + } + + /** + * Get the locked section length + * + * @return long Locked section length + */ + public final long getLength() + { + return m_length; + } + + /** + * Set the locked section length + * + * @param long Locked section length + */ + public final void setLength(long len) + { + if (len < 0) + m_length = LockWholeFile; + else + m_length = len; + } + + /** + * Get the owner process id for the lock + * + * @return int + */ + public final int getProcessId() + { + return m_pid; + } + + /** + * Deterine if the lock is locking the whole file + * + * @return boolean + */ + public final boolean isWholeFile() + { + return m_length == LockWholeFile ? true : false; + } + + /** + * Set the process id of the owner of this lock + * + * @param pid int + */ + public final void setProcessId(int pid) + { + m_pid = pid; + } + + /** + * Check if the specified locks byte range overlaps this locks byte range. + * + * @param lock FileLock + */ + public final boolean hasOverlap(FileLock lock) + { + return hasOverlap(lock.getOffset(), lock.getLength()); + } + + /** + * Check if the specified locks byte range overlaps this locks byte range. + * + * @param offset long + * @param len long + */ + public final boolean hasOverlap(long offset, long len) + { + + // Check if the lock is for the whole file + + if (isWholeFile()) + return true; + + // Check if the locks overlap + + long endOff = getOffset() + getLength(); + + if (getOffset() < offset && endOff < offset) + return false; + + endOff = offset + len; + + if (getOffset() > endOff) + return false; + + // Locks overlap + + return true; + } + + /** + * Return the lock details as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("[PID="); + str.append(getProcessId()); + str.append(",Offset="); + str.append(getOffset()); + str.append(",Len="); + str.append(getLength()); + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/locking/FileLockException.java b/source/java/org/alfresco/filesys/locking/FileLockException.java new file mode 100644 index 0000000000..112912e0cb --- /dev/null +++ b/source/java/org/alfresco/filesys/locking/FileLockException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.locking; + +import java.io.IOException; + +/** + * File Lock Exception Class + */ +public class FileLockException extends IOException +{ + private static final long serialVersionUID = 3257845472092893751L; + + /** + * Class constructor. + */ + public FileLockException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public FileLockException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/locking/FileLockList.java b/source/java/org/alfresco/filesys/locking/FileLockList.java new file mode 100644 index 0000000000..0cff266852 --- /dev/null +++ b/source/java/org/alfresco/filesys/locking/FileLockList.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.locking; + +import java.util.Vector; + +/** + * File Lock List Class + *

    + * Contains a list of the current locks on a file. + */ +public class FileLockList +{ + + // List of file locks + + private Vector m_lockList; + + /** + * Construct an empty file lock list. + */ + public FileLockList() + { + m_lockList = new Vector(); + } + + /** + * Add a lock to the list + * + * @param lock Lock to be added to the list. + */ + public final void addLock(FileLock lock) + { + m_lockList.add(lock); + } + + /** + * Remove a lock from the list + * + * @param lock FileLock + * @return FileLock + */ + public final FileLock removeLock(FileLock lock) + { + return removeLock(lock.getOffset(), lock.getLength(), lock.getProcessId()); + } + + /** + * Remove a lock from the list + * + * @param long offset Starting offset of the lock + * @param long len Locked section length + * @param int pid Owner process id + * @return FileLock + */ + public final FileLock removeLock(long offset, long len, int pid) + { + + // Check if there are any locks in the list + + if (numberOfLocks() == 0) + return null; + + // Search for the required lock + + for (int i = 0; i < numberOfLocks(); i++) + { + + // Get the current lock details + + FileLock curLock = getLockAt(i); + if (curLock.getOffset() == offset && curLock.getLength() == len && curLock.getProcessId() == pid) + { + + // Remove the lock from the list + + m_lockList.removeElementAt(i); + return curLock; + } + } + + // Lock not found + + return null; + } + + /** + * Remove all locks from the list + */ + public final void removeAllLocks() + { + m_lockList.removeAllElements(); + } + + /** + * Return the specified lock details + * + * @param int Lock index + * @return FileLock + */ + public final FileLock getLockAt(int idx) + { + if (idx < m_lockList.size()) + return m_lockList.elementAt(idx); + return null; + } + + /** + * Check if the new lock should be allowed by comparing with the locks in the list. + * + * @param lock FileLock + * @return boolean true if the lock can be granted, else false. + */ + public final boolean allowsLock(FileLock lock) + { + + // If the list is empty we can allow the lock request + + if (numberOfLocks() == 0) + return true; + + // Search for any overlapping locks + + for (int i = 0; i < numberOfLocks(); i++) + { + + // Get the current lock details + + FileLock curLock = getLockAt(i); + if (curLock.hasOverlap(lock)) + return false; + } + + // The lock does not overlap with any existing locks + + return true; + } + + /** + * Check if the file is readable for the specified section of the file and process id + * + * @param offset long + * @param len long + * @param pid int + * @return boolean + */ + public final boolean canReadFile(long offset, long len, int pid) + { + + // If the list is empty we can allow the read request + + if (numberOfLocks() == 0) + return true; + + // Search for a lock that prevents the read + + for (int i = 0; i < numberOfLocks(); i++) + { + + // Get the current lock details + + FileLock curLock = getLockAt(i); + + // Check if the process owns the lock, if not then check if there is an overlap + + if (curLock.getProcessId() != pid) + { + + // Check if the read overlaps with the locked area + + if (curLock.hasOverlap(offset, len) == true) + return false; + } + } + + // The lock does not overlap with any existing locks + + return true; + } + + /** + * Check if the file is writeable for the specified section of the file and process id + * + * @param offset long + * @param len long + * @param pid int + * @return boolean + */ + public final boolean canWriteFile(long offset, long len, int pid) + { + + // If the list is empty we can allow the read request + + if (numberOfLocks() == 0) + return true; + + // Search for a lock that prevents the read + + for (int i = 0; i < numberOfLocks(); i++) + { + + // Get the current lock details + + FileLock curLock = getLockAt(i); + + // Check if the process owns the lock, if not then check if there is an overlap + + if (curLock.getProcessId() != pid) + { + + // Check if the read overlaps with the locked area + + if (curLock.hasOverlap(offset, len) == true) + return false; + } + } + + // The lock does not overlap with any existing locks + + return true; + } + + /** + * Return the count of locks in the list. + * + * @return int Number of locks in the list. + */ + public final int numberOfLocks() + { + return m_lockList.size(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/locking/FileUnlockException.java b/source/java/org/alfresco/filesys/locking/FileUnlockException.java new file mode 100644 index 0000000000..557c250542 --- /dev/null +++ b/source/java/org/alfresco/filesys/locking/FileUnlockException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.locking; + +import java.io.IOException; + +/** + * File Unlock Exception Class + */ +public class FileUnlockException extends IOException +{ + private static final long serialVersionUID = 3257290240262484786L; + + /** + * Class constructor. + */ + public FileUnlockException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public FileUnlockException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/locking/LockConflictException.java b/source/java/org/alfresco/filesys/locking/LockConflictException.java new file mode 100644 index 0000000000..d7c1f0e732 --- /dev/null +++ b/source/java/org/alfresco/filesys/locking/LockConflictException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.locking; + +import java.io.IOException; + +/** + * Lock Conflict Exception Class + *

    + * Thrown when a lock request overlaps with an existing lock on a file. + */ +public class LockConflictException extends IOException +{ + + // Serializable version id + + private static final long serialVersionUID = 0; + + /** + * Class constructor. + */ + public LockConflictException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public LockConflictException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/locking/NotLockedException.java b/source/java/org/alfresco/filesys/locking/NotLockedException.java new file mode 100644 index 0000000000..67d7f7b488 --- /dev/null +++ b/source/java/org/alfresco/filesys/locking/NotLockedException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.locking; + +import java.io.IOException; + +/** + * Not Locked Exception Class + *

    + * Thrown when an unlock request is received that has not active lock on a file. + */ +public class NotLockedException extends IOException +{ + private static final long serialVersionUID = 3834594296543261488L; + + /** + * Class constructor. + */ + public NotLockedException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public NotLockedException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/NameTemplateException.java b/source/java/org/alfresco/filesys/netbios/NameTemplateException.java new file mode 100644 index 0000000000..403865bbb8 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NameTemplateException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +/** + * Name Template Exception Class + *

    + * Thrown when a NetBIOS name template contains invalid characters or is too long. + */ +public class NameTemplateException extends Exception +{ + private static final long serialVersionUID = 3256439188231762230L; + + /** + * Default constructor. + */ + public NameTemplateException() + { + super(); + } + + /** + * Class constructor + * + * @param s java.lang.String + */ + public NameTemplateException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSDatagram.java b/source/java/org/alfresco/filesys/netbios/NetBIOSDatagram.java new file mode 100644 index 0000000000..0cc4fcb344 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSDatagram.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.alfresco.filesys.util.DataPacker; + +/** + * NetBIOS datagram class. + */ +public class NetBIOSDatagram +{ + // Datagram types + + public static final int DIRECT_UNIQUE = 0x10; + public static final int DIRECT_GROUP = 0x11; + public static final int BROADCAST = 0x12; + public static final int DATAGRAM_ERROR = 0x13; + public static final int DATAGRAM_QUERY = 0x14; + public static final int POSITIVE_RESP = 0x15; + public static final int NEGATIVE_RESP = 0x16; + + // Datagram flags + + public static final int FLG_MOREFRAGMENTS = 0x01; + public static final int FLG_FIRSTPKT = 0x02; + + // Default NetBIOS packet buffer size to allocate + + public static final int DEFBUFSIZE = 4096; + + // NetBIOS datagram offsets + + public static final int NB_MSGTYPE = 0; + public static final int NB_FLAGS = 1; + public static final int NB_DATAGRAMID = 2; + public static final int NB_SOURCEIP = 4; + public static final int NB_SOURCEPORT = 8; + public static final int NB_DATAGRAMLEN = 10; + public static final int NB_PKTOFFSET = 12; + public static final int NB_FROMNAME = 14; + public static final int NB_TONAME = 48; + public static final int NB_USERDATA = 82; + + public static final int NB_MINLENGTH = 82; + public static final int NB_MINSMBLEN = 100; + + // NetBIOS packet buffer + + protected byte[] m_buf; + + // Next available datagram id + + private static int m_nextId; + + /** + * NetBIOS Datagram constructor + */ + + public NetBIOSDatagram() + { + + // Allocaet a NetBIOS packet buffer + + m_buf = new byte[DEFBUFSIZE]; + } + + /** + * Create a new NetBIOS datagram using the specified packet buffer. + * + * @param pkt byte[] + */ + public NetBIOSDatagram(byte[] pkt) + { + m_buf = pkt; + } + + /** + * Create a new NetBIOS datagram with the specified buffer size. + * + * @param bufSize int + */ + public NetBIOSDatagram(int bufSize) + { + m_buf = new byte[bufSize]; + } + + /** + * Return the next available datagram id. + */ + public final static synchronized int getNextDatagramId() + { + + // Update and return the next available datagram id + + return m_nextId++; + } + + /** + * Return the NetBIOS buffer. + * + * @return byte[] + */ + public final byte[] getBuffer() + { + return m_buf; + } + + /** + * Get the datagram id. + * + * @return int + */ + public final int getDatagramId() + { + return DataPacker.getIntelShort(m_buf, NB_DATAGRAMID); + } + + /** + * Get the datagram destination name. + * + * @return NetBIOSName + */ + public final NetBIOSName getDestinationName() + { + + // Decode the NetBIOS name to a string + + String name = NetBIOSSession.DecodeName(m_buf, NB_TONAME + 1); + if (name != null) + { + + // Convert the name string to a NetBIOS name + + NetBIOSName nbName = new NetBIOSName(name.substring(0, 14), name.charAt(15), false); + if (getMessageType() == DIRECT_GROUP) + nbName.setGroup(true); + return nbName; + } + return null; + } + + /** + * Return the datagram flags value. + * + * @return int + */ + public final int getFlags() + { + return m_buf[NB_FLAGS] & 0xFF; + } + + /** + * Return the datagram length. + * + * @return int + */ + public final int getLength() + { + return DataPacker.getShort(m_buf, NB_DATAGRAMLEN); + } + + /** + * Return the user data length + * + * @return int + */ + public final int getDataLength() + { + return getLength() - NB_USERDATA; + } + + /** + * Get the NetBIOS datagram message type. + * + * @return int + */ + public final int getMessageType() + { + return m_buf[NB_MSGTYPE] & 0xFF; + } + + /** + * Return the datagram source IP address. + * + * @return byte[] + */ + public final byte[] getSourceIPAddress() + { + + // Allocate a 4 byte array for the IP address + + byte[] ipaddr = new byte[4]; + + // Copy the IP address bytes from the datagram + + for (int i = 0; i < 4; i++) + ipaddr[i] = m_buf[NB_SOURCEIP + i]; + + // Return the IP address bytes + + return ipaddr; + } + + /** + * Return the datagram source IP address, as a string + * + * @return String + */ + public final String getSourceAddress() + { + + // Get the IP address + + byte[] addr = getSourceIPAddress(); + + // Build the IP address string + + StringBuffer addrStr = new StringBuffer(); + + addrStr.append(addr[0]); + addrStr.append("."); + addrStr.append(addr[1]); + addrStr.append("."); + addrStr.append(addr[2]); + addrStr.append("."); + addrStr.append(addr[3]); + + return addrStr.toString(); + } + + /** + * Get the source NetBIOS name. + * + * @return java.lang.String + */ + public final NetBIOSName getSourceName() + { + + // Decode the NetBIOS name string + + String name = NetBIOSSession.DecodeName(m_buf, NB_FROMNAME + 1); + + // Convert the name to a NetBIOS name + + if (name != null) + { + + // Convert the name string to a NetBIOS name + + NetBIOSName nbName = new NetBIOSName(name.substring(0, 14), name.charAt(15), false); + return nbName; + } + return null; + } + + /** + * Get the source port/socket for the datagram. + * + * @return int + */ + public final int getSourcePort() + { + return DataPacker.getIntelShort(m_buf, NB_SOURCEPORT); + } + + /** + * Check if the user data is an SMB packet + * + * @return boolean + */ + public final boolean isSMBData() + { + if (m_buf[NB_USERDATA] == (byte) 0xFF && m_buf[NB_USERDATA + 1] == (byte) 'S' + && m_buf[NB_USERDATA + 2] == (byte) 'M' && m_buf[NB_USERDATA + 3] == (byte) 'B' + && getLength() >= NB_MINSMBLEN) + return true; + return false; + } + + /** + * Return the message type as a string + * + * @return String + */ + + public final String getMessageTypeString() + { + + // Determine the message type + + String typ = null; + + switch (getMessageType()) + { + case DIRECT_GROUP: + typ = "DIRECT GROUP"; + break; + case DIRECT_UNIQUE: + typ = "DIRECT UNIQUE"; + break; + case DATAGRAM_ERROR: + typ = "DATAGRAM ERROR"; + break; + case DATAGRAM_QUERY: + typ = "DATAGRAM QUERY"; + break; + case BROADCAST: + typ = "BROADCAST"; + break; + case POSITIVE_RESP: + typ = "POSITIVE RESP"; + break; + case NEGATIVE_RESP: + typ = "NEGATIVE RESP"; + break; + default: + typ = "UNKNOWN"; + break; + } + + // Return the message type string + + return typ; + } + + /** + * Send a datagram to the specified NetBIOS name using the global NetBIOS datagram socket + * + * @param dgramTyp Datagram type + * @param fromName From NetBIOS name + * @param fromNameTyp From NetBIOS name type. + * @param toName To NetBIOS name + * @param toNameType To NetBIOS name type. + * @param userData User data buffer + * @param userLen User data length. + * @param userOff Offset of data within user buffer. + * @param addr Address to send to + * @param port Port to send to + * @exception java.io.IOException Error occurred sending datagram + * @exception UnknownHostException Failed to generate the broadcast mask for the network + */ + public final void SendDatagram(int dgramTyp, String fromName, char fromNameType, String toName, char toNameType, + byte[] userData, int userLen, int userOff, InetAddress addr, int port) throws IOException, + UnknownHostException + { + + // Set the datagram header values + + setMessageType(dgramTyp); + setSourceName(fromName, fromNameType); + setDestinationName(toName, toNameType); + setSourcePort(RFCNetBIOSProtocol.DATAGRAM); + setSourceIPAddress(InetAddress.getLocalHost().getAddress()); + setFlags(FLG_FIRSTPKT); + + if (m_nextId == 0) + m_nextId = (int) (System.currentTimeMillis() & 0x7FFF); + setDatagramId(m_nextId++); + + // Set the user data and length + + setLength(userLen + NB_USERDATA); + setUserData(userData, userLen, userOff); + + // Use the global NetBIOS datagram socket to sent the broadcast datagram + + NetBIOSDatagramSocket nbSocket = NetBIOSDatagramSocket.getInstance(); + nbSocket.sendDatagram(this, addr, port); + } + + /** + * Send a datagram to the specified NetBIOS name using the global NetBIOS datagram socket + * + * @param dgramTyp Datagram type + * @param fromName From NetBIOS name + * @param fromNameTyp From NetBIOS name type. + * @param toName To NetBIOS name + * @param toNameType To NetBIOS name type. + * @param userData User data buffer + * @param userLen User data length. + * @param userOff Offset of data within user buffer. + * @exception java.io.IOException Error occurred sending datagram + * @exception UnknownHostException Failed to generate the broadcast mask for the network + */ + public final void SendDatagram(int dgramTyp, String fromName, char fromNameType, String toName, char toNameType, + byte[] userData, int userLen, int userOff) throws IOException, UnknownHostException + { + + // Set the datagram header values + + setMessageType(dgramTyp); + setSourceName(fromName, fromNameType); + setDestinationName(toName, toNameType); + setSourcePort(RFCNetBIOSProtocol.DATAGRAM); + setSourceIPAddress(InetAddress.getLocalHost().getAddress()); + setFlags(FLG_FIRSTPKT); + + if (m_nextId == 0) + m_nextId = (int) (System.currentTimeMillis() & 0x7FFF); + setDatagramId(m_nextId++); + + // Set the user data and length + + setLength(userLen + NB_USERDATA); + setUserData(userData, userLen, userOff); + + // Use the global NetBIOS datagram socket to sent the broadcast datagram + + NetBIOSDatagramSocket nbSocket = NetBIOSDatagramSocket.getInstance(); + nbSocket.sendBroadcastDatagram(this); + } + + /** + * Send a datagram to the specified NetBIOS name using the global NetBIOS datagram socket + * + * @param dgramTyp Datagram type + * @param fromName From NetBIOS name + * @param fromNameTyp From NetBIOS name type. + * @param toName To NetBIOS name + * @param toNameType To NetBIOS name type. + * @param userData User data buffer + * @param userLen User data length. + * @exception java.io.IOException Error occurred sending datagram + * @exception UnknownHostException Failed to generate the broadcast mask for the network + */ + public final void SendDatagram(int dgramTyp, String fromName, String toName, byte[] userData, int userLen) + throws IOException, UnknownHostException + { + + // Send the datagram from the standard port + + SendDatagram(dgramTyp, fromName, NetBIOSName.FileServer, toName, NetBIOSName.FileServer, userData, userLen, 0); + } + + /** + * Send a datagram to the specified NetBIOS name using the supplised datagram socket. + * + * @param dgramTyp Datagram type + * @param sock Datagram socket to use to send the datagram packet. + * @param fromName From NetBIOS name + * @param fromNameTyp From NetBIOS name type. + * @param toName To NetBIOS name + * @param toNameType To NetBIOS name type. + * @param userData User data buffer + * @param userLen User data length. + * @param userOff Offset of data within user buffer. + * @exception java.io.IOException The exception description. + */ + public final void SendDatagram(int dgramTyp, DatagramSocket sock, String fromName, char fromNameType, + String toName, char toNameType, byte[] userData, int userLen, int userOff) throws IOException + { + + // Set the datagram header values + + setMessageType(dgramTyp); + setSourceName(fromName, fromNameType); + setDestinationName(toName, toNameType); + setSourcePort(RFCNetBIOSProtocol.DATAGRAM); + setSourceIPAddress(InetAddress.getLocalHost().getAddress()); + setFlags(FLG_FIRSTPKT); + + if (m_nextId == 0) + m_nextId = (int) (System.currentTimeMillis() & 0x7FFF); + setDatagramId(m_nextId++); + + // Set the user data and length + + setLength(userLen + NB_USERDATA); + setUserData(userData, userLen, userOff); + + // Build a broadcast destination address + + InetAddress destAddr = InetAddress.getByName(NetworkSettings.GenerateBroadcastMask(null)); + DatagramPacket dgram = new DatagramPacket(m_buf, userLen + NB_USERDATA, destAddr, RFCNetBIOSProtocol.DATAGRAM); + + // Debug + + // HexDump.Dump( m_buf, userLen + NB_USERDATA, 0); + + // Send the datagram + + sock.send(dgram); + } + + /** + * Send a datagram to the specified NetBIOS name using the supplied datagram socket. + * + * @param fromName java.lang.String + * @param toName java.lang.String + * @param userData byte[] + * @param userLen int + * @exception java.io.IOException The exception description. + */ + public final void SendDatagram(int dgramTyp, DatagramSocket sock, String fromName, String toName, byte[] userData, + int userLen) throws IOException + { + + // Send the datagram from the standard port + + SendDatagram(dgramTyp, sock, fromName, NetBIOSName.FileServer, toName, NetBIOSName.FileServer, userData, + userLen, 0); + } + + /** + * Set the datagram id. + * + * @param id int + */ + public final void setDatagramId(int id) + { + DataPacker.putIntelShort(id, m_buf, NB_DATAGRAMID); + } + + /** + * Set the datagram destination name. + * + * @param name java.lang.String + */ + public final void setDestinationName(String name) + { + setDestinationName(name, NetBIOSName.FileServer); + } + + /** + * Set the datagram destination name. + * + * @param name java.lang.String + */ + public final void setDestinationName(String name, char typ) + { + + // Convert the name to NetBIOS RFC encoded name + + NetBIOSSession.EncodeName(name, typ, m_buf, NB_TONAME); + } + + /** + * Set the datagram flags value. + * + * @param flg int + */ + public final void setFlags(int flg) + { + m_buf[NB_FLAGS] = (byte) (flg & 0xFF); + } + + /** + * Set the datagram length. + * + * @param len int + */ + public final void setLength(int len) + { + DataPacker.putShort((short) len, m_buf, NB_DATAGRAMLEN); + } + + /** + * Set the NetBIOS datagram message type. + * + * @param msg int + */ + public final void setMessageType(int msg) + { + m_buf[NB_MSGTYPE] = (byte) (msg & 0xFF); + } + + /** + * Set the source IP address for the datagram. + * + * @param ipaddr byte[] + */ + public final void setSourceIPAddress(byte[] ipaddr) + { + + // Pack the IP address into the datagram buffer + + for (int i = 0; i < 4; i++) + m_buf[NB_SOURCEIP + i] = ipaddr[i]; + } + + /** + * Set the datagram source NetBIOS name. + * + * @param name java.lang.String + */ + public final void setSourceName(String name) + { + + // Convert the name to NetBIOS RFC encoded name + + NetBIOSSession.EncodeName(name, NetBIOSName.FileServer, m_buf, NB_FROMNAME); + } + + /** + * Set the datagram source NetBIOS name. + * + * @param name java.lang.String + */ + public final void setSourceName(String name, char typ) + { + + // Convert the name to NetBIOS RFC encoded name + + NetBIOSSession.EncodeName(name, typ, m_buf, NB_FROMNAME); + } + + /** + * Set the source port/socket for the datagram. + * + * @param port int + */ + public final void setSourcePort(int port) + { + DataPacker.putShort((short) port, m_buf, NB_SOURCEPORT); + } + + /** + * Set the user data portion of the datagram. + * + * @param buf byte[] + * @param len int + */ + public final void setUserData(byte[] buf, int len) + { + + // Copy the user data + + System.arraycopy(buf, 0, m_buf, NB_USERDATA, len); + } + + /** + * Set the user data portion of the datagram. + * + * @param buf User data buffer + * @param len Length of user data + * @param off Offset to start of data within buffer. + */ + public final void setUserData(byte[] buf, int len, int off) + { + + // Copy the user data + + System.arraycopy(buf, off, m_buf, NB_USERDATA, len); + } + + /** + * Common constructor initialization code. + */ + protected final void CommonInit() + { + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSDatagramSocket.java b/source/java/org/alfresco/filesys/netbios/NetBIOSDatagramSocket.java new file mode 100644 index 0000000000..77a2b57327 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSDatagramSocket.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +/** + * NetBIOS Datagram Socket Class + *

    + * Singleton class that allows multiple users of the socket. + */ +public class NetBIOSDatagramSocket +{ + // Global NetBIOS datagram socket instance + + private static NetBIOSDatagramSocket m_nbSocket; + + // Default port and bind address + + private static int m_defPort = RFCNetBIOSProtocol.DATAGRAM; + private static InetAddress m_defBindAddr; + + // Datagram socket + + private DatagramSocket m_socket; + + // Broadcast address + + private InetAddress m_broadcastAddr; + + /** + * Class constructor + * + * @exception SocketException + * @exception UnknownHostException + */ + private NetBIOSDatagramSocket() throws SocketException, UnknownHostException + { + + // Create the datagram socket + + if (m_defBindAddr == null) + m_socket = new DatagramSocket(m_defPort); + else + m_socket = new DatagramSocket(m_defPort, m_defBindAddr); + + // Generate the broadcast mask + + if (m_defBindAddr == null) + m_broadcastAddr = InetAddress.getByName(NetworkSettings.GenerateBroadcastMask(null)); + else + m_broadcastAddr = InetAddress.getByName(NetworkSettings.GenerateBroadcastMask(m_defBindAddr + .getHostAddress())); + } + + /** + * Return the global NetBIOS datagram instance + * + * @return NetBIOSDatagramSocket + * @exception SocketException + * @exception UnknownHostException + */ + public final static synchronized NetBIOSDatagramSocket getInstance() throws SocketException, UnknownHostException + { + + // Check if the datagram socket has been created + + if (m_nbSocket == null) + m_nbSocket = new NetBIOSDatagramSocket(); + + // Return the global NetBIOS datagram socket instance + + return m_nbSocket; + } + + /** + * Set the default port to use + * + * @param port int + */ + public final static void setDefaultPort(int port) + { + m_defPort = port; + } + + /** + * Set the address to bind the datagram socket to + * + * @param bindAddr InetAddress + */ + public final static void setBindAddress(InetAddress bindAddr) + { + m_defBindAddr = bindAddr; + } + + /** + * Receive a NetBIOS datagram + * + * @param dgram NetBIOSDatagram + * @return int + * @exception IOException + */ + public final int receiveDatagram(NetBIOSDatagram dgram) throws IOException + { + + // Create a datagram packet using the NetBIOS datagram buffer + + DatagramPacket pkt = new DatagramPacket(dgram.getBuffer(), dgram.getBuffer().length); + + // Receive a datagram + + m_socket.receive(pkt); + return pkt.getLength(); + } + + /** + * Send a NetBIOS datagram + * + * @param dgram NetBIOSDatagram + * @param destAddr InetAddress + * @param destPort int + * @exception IOException + */ + public final void sendDatagram(NetBIOSDatagram dgram, InetAddress destAddr, int destPort) throws IOException + { + + // Create a datagram packet using the NetBIOS datagram buffer + + DatagramPacket pkt = new DatagramPacket(dgram.getBuffer(), dgram.getLength(), destAddr, destPort); + + // Send the NetBIOS datagram + + m_socket.send(pkt); + } + + /** + * Send a broadcast NetBIOS datagram + * + * @param dgram NetBIOSDatagram + * @exception IOException + */ + public final void sendBroadcastDatagram(NetBIOSDatagram dgram) throws IOException + { + + // Create a datagram packet using the NetBIOS datagram buffer + + DatagramPacket pkt = new DatagramPacket(dgram.getBuffer(), dgram.getLength(), m_broadcastAddr, m_defPort); + + // Send the NetBIOS datagram + + m_socket.send(pkt); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSException.java b/source/java/org/alfresco/filesys/netbios/NetBIOSException.java new file mode 100644 index 0000000000..67a1a860fd --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +/** + * NetBIOS exception class. + */ +public class NetBIOSException extends Exception +{ + private static final long serialVersionUID = 3256438122995988025L; + + /** + * NetBIOSException constructor comment. + */ + public NetBIOSException() + { + super(); + } + + /** + * NetBIOSException constructor comment. + * + * @param s java.lang.String + */ + public NetBIOSException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSName.java b/source/java/org/alfresco/filesys/netbios/NetBIOSName.java new file mode 100644 index 0000000000..486648449e --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSName.java @@ -0,0 +1,1049 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import java.net.InetAddress; +import java.util.StringTokenizer; +import java.util.Vector; + +import org.alfresco.filesys.util.IPAddress; + +/** + * NetBIOS Name Class. + */ +public class NetBIOSName +{ + // NetBIOS name length + + public static final int NameLength = 16; + + // NetBIOS name types - + type + + public static final char WorkStation = 0x00; + public static final char Messenger = 0x01; + public static final char RemoteMessenger = 0x03; + public static final char RASServer = 0x06; + public static final char FileServer = 0x20; + public static final char RASClientService = 0x21; + public static final char MSExchangeInterchange = 0x22; + public static final char MSExchangeStore = 0x23; + public static final char MSExchangeDirectory = 0x24; + public static final char LotusNotesServerService= 0x2B; + public static final char ModemSharingService = 0x30; + public static final char ModemSharingClient = 0x31; + public static final char McCaffeeAntiVirus = 0x42; + public static final char SMSClientRemoteControl = 0x43; + public static final char SMSAdminRemoteControl = 0x44; + public static final char SMSClientRemoteChat = 0x45; + public static final char SMSClientRemoteTransfer= 0x46; + public static final char DECPathworksService = 0x4C; + public static final char MSExchangeIMC = 0x6A; + public static final char MSExchangeMTA = 0x87; + public static final char NetworkMonitorAgent = 0xBE; + public static final char NetworkMonitorApp = 0xBF; + + // + type + + public static final char Domain = 0x00; // Group + public static final char DomainMasterBrowser = 0x1B; + public static final char DomainControllers = 0x1C; // Group + public static final char MasterBrowser = 0x1D; + public static final char DomainAnnounce = 0x1E; + + // Browse master - __MSBROWSE__ + type + + public static final char BrowseMasterGroup = 0x01; + + // Browse master NetBIOS name + + public static final String BrowseMasterName = "\u0001\u0002__MSBROWSE__\u0002"; + + // NetBIOS names + + public static final String SMBServer = "*SMBSERVER"; + public static final String SMBServer2 = "*SMBSERV"; + + // Default time to live for name registrations + + public static final int DefaultTTL = 28800; // 8 hours + + // Name conversion string + + private static final String EncodeConversion = "ABCDEFGHIJKLMNOP"; + + // Character set to use when converting the NetBIOS name string to a byte array + + private static String _nameConversionCharset = null; + + // Name string and type + + private String m_name; + private char m_type; + + // Name scope + + private String m_scope; + + // Group name flag + + private boolean m_group = false; + + // Local name flag + + private boolean m_local = true; + + // IP address(es) of the owner of this name + + private Vector m_addrList; + + // Time that the name expires and time to live + + private long m_expiry; + private int m_ttl; // seconds + + /** + * Create a unique NetBIOS name. + * + * @param name java.lang.String + * @param typ char + * @param group + */ + public NetBIOSName(String name, char typ, boolean group) + { + setName(name); + setType(typ); + setGroup(group); + } + + /** + * Create a unique NetBIOS name. + * + * @param name java.lang.String + * @param typ char + * @param group boolean + * @param ipaddr byte[] + */ + public NetBIOSName(String name, char typ, boolean group, byte[] ipaddr) + { + setName(name); + setType(typ); + setGroup(group); + addIPAddress(ipaddr); + } + + /** + * Create a unique NetBIOS name. + * + * @param name java.lang.String + * @param typ char + * @param group boolean + * @param ipList Vector + */ + public NetBIOSName(String name, char typ, boolean group, Vector ipList) + { + setName(name); + setType(typ); + setGroup(group); + addIPAddresses(ipList); + } + + /** + * Create a unique NetBIOS name. + * + * @param name java.lang.String + * @param typ char + * @param group boolean + * @param ipaddr byte[] + * @param ttl int + */ + public NetBIOSName(String name, char typ, boolean group, byte[] ipaddr, int ttl) + { + setName(name); + setType(typ); + setGroup(group); + addIPAddress(ipaddr); + setTimeToLive(ttl); + } + + /** + * Create a unique NetBIOS name. + * + * @param name java.lang.String + * @param typ char + * @param group boolean + * @param ipList Vector + * @param ttl int + */ + public NetBIOSName(String name, char typ, boolean group, Vector ipList, int ttl) + { + setName(name); + setType(typ); + setGroup(group); + addIPAddresses(ipList); + setTimeToLive(ttl); + } + + /** + * Create a NetBIOS name from a byte array + * + * @param buf byte[] + * @param off int + */ + public NetBIOSName(byte[] buf, int off) + { + setName(new String(buf, off, NameLength - 1)); + setType((char) buf[off + NameLength - 1]); + } + + /** + * Create a NetBIOS name from an encoded name string + * + * @param name String + */ + public NetBIOSName(String name) + { + setName(name.substring(0, NameLength - 1).trim()); + setType(name.charAt(NameLength - 1)); + } + + /** + * Create a NetBIOS name from the specified name and scope + * + * @param name String + * @param scope String + */ + protected NetBIOSName(String name, String scope) + { + setName(name.substring(0, NameLength - 1).trim()); + setType(name.charAt(NameLength - 1)); + + if (scope != null && scope.length() > 0) + setNameScope(scope); + } + + /** + * Compare objects for equality. + * + * @return boolean + * @param obj java.lang.Object + */ + public boolean equals(Object obj) + { + + // Check if the object is a NetBIOSName type object + + if (obj instanceof NetBIOSName) + { + + // Check if the NetBIOS name, name type and local/remote flags are equal + + NetBIOSName nbn = (NetBIOSName) obj; + if (nbn.getName().equals(getName()) && nbn.getType() == getType() && nbn.isLocalName() == isLocalName()) + return true; + } + + // Objects are not equal + + return false; + } + + /** + * Return the system time that the NetBIOS name expires. + * + * @return long + */ + public final long getExpiryTime() + { + return m_expiry; + } + + /** + * Get the names time to live value, in seconds + * + * @return int + */ + public final int getTimeToLive() + { + return m_ttl; + } + + /** + * Return the number of addresses for this NetBIOS name + * + * @return int + */ + public final int numberOfAddresses() + { + return m_addrList != null ? m_addrList.size() : 0; + } + + /** + * Return the specified IP address that owns the NetBIOS name. + * + * @param idx int + * @return byte[] + */ + public final byte[] getIPAddress(int idx) + { + if (m_addrList == null || idx < 0 || idx >= m_addrList.size()) + return null; + return m_addrList.get(idx); + } + + /** + * Return the specified IP address that owns the NetBIOS name, as a string. + * + * @param idx int + * @return String + */ + public final String getIPAddressString(int idx) + { + if (m_addrList == null || idx < 0 || idx >= m_addrList.size()) + return null; + + // Get the raw IP address and build the address string + + return IPAddress.asString(m_addrList.get(idx)); + } + + /** + * Return the NetBIOS name. + * + * @return java.lang.String + */ + public final String getName() + { + return m_name; + } + + /** + * Return the NetBIOS name as a 16 character string with the name and type + * + * @return byte[] + */ + public final byte[] getNetBIOSName() + { + + // Allocate a buffer to build the full name + + byte[] nameBuf = new byte[NameLength]; + + // Get the name string bytes + + byte[] nameBytes = null; + + try + { + if (hasNameConversionCharacterSet()) + nameBytes = getName().getBytes(getNameConversionCharacterSet()); + else + nameBytes = getName().getBytes(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + + for (int i = 0; i < nameBytes.length; i++) + nameBuf[i] = nameBytes[i]; + for (int i = nameBytes.length; i < NameLength; i++) + nameBuf[i] = ' '; + nameBuf[NameLength - 1] = (byte) (m_type & 0xFF); + + return nameBuf; + } + + /** + * Determine if the name has a name scope + * + * @return boolean + */ + public final boolean hasNameScope() + { + return m_scope != null ? true : false; + } + + /** + * Return the name scope + * + * @return String + */ + public final String getNameScope() + { + return m_scope; + } + + /** + * Return the NetBIOS name type. + * + * @return char + */ + public final char getType() + { + return m_type; + } + + /** + * Return a hash code for this object. + * + * @return int + */ + public int hashCode() + { + return getName().hashCode() + (int) getType(); + } + + /** + * Returns true if this is a group type NetBIOS name. + * + * @return boolean + */ + public final boolean isGroupName() + { + return m_group; + } + + /** + * Determine if this is a local or remote NetBIOS name. + * + * @return boolean + */ + public final boolean isLocalName() + { + return m_local; + } + + /** + * Returns true if the NetBIOS name is a unique type name. + * + * @return boolean + */ + public final boolean isUniqueName() + { + return m_group ? false : true; + } + + /** + * Remove all TCP/IP addresses from the NetBIOS name + */ + public final void removeAllAddresses() + { + m_addrList.removeAllElements(); + } + + /** + * Set the system time that this NetBIOS name expires at. + * + * @param expires long + */ + public final void setExpiryTime(long expires) + { + m_expiry = expires; + } + + /** + * Set the names time to live, in seconds + * + * @param ttl int + */ + public final void setTimeToLive(int ttl) + { + m_ttl = ttl; + } + + /** + * Set/clear the group name flag. + * + * @param flag boolean + */ + public final void setGroup(boolean flag) + { + m_group = flag; + } + + /** + * Set the name scope + * + * @param scope String + */ + public final void setNameScope(String scope) + { + if (scope == null) + m_scope = null; + else if (scope.length() > 0 && scope.startsWith(".")) + m_scope = scope.substring(1); + else + m_scope = scope; + } + + /** + * Add an IP address to the list of addresses for this NetBIOS name + * + * @param ipaddr byte[] + */ + public final void addIPAddress(byte[] ipaddr) + { + if (m_addrList == null) + m_addrList = new Vector(); + m_addrList.add(ipaddr); + } + + /** + * Add a list of IP addresses to the list of addresses for this NetBIOS name + * + * @param ipaddr Vector + */ + public final void addIPAddresses(Vector addrList) + { + if (m_addrList == null) + m_addrList = new Vector(); + + // Add the addresses + + for (int i = 0; i < addrList.size(); i++) + { + byte[] addr = addrList.get(i); + m_addrList.add(addr); + } + } + + /** + * Set the local/remote NetBIOS name flag. + * + * @param local boolean + */ + public final void setLocalName(boolean local) + { + m_local = local; + } + + /** + * Set the NetBIOS name. + * + * @param name java.lang.String + */ + public final void setName(String name) + { + + // Check if the name contains a name scope, if so then split the name and scope id + + int pos = name.indexOf("."); + if (pos != -1) + { + + // Split the name and scope id + + setNameScope(name.substring(pos + 1)); + m_name = toUpperCaseName(name.substring(0, pos)); + } + else + { + + // Set the name + + m_name = toUpperCaseName(name); + } + } + + /** + * Set the NetBIOS name type. + * + * @param typ char + */ + public final void setType(char typ) + { + m_type = typ; + } + + /** + * Convert a name to uppercase + * + * @return String + */ + public static String toUpperCaseName(String name) + { + + // Trim the name, unless it looks like a special name + + if (name.length() > 2 && name.charAt(0) != 0x01 && name.charAt(1) != 0x02) + name = name.trim(); + + // Convert the string to uppercase + + if (name != null && name.length() > 0) + { + StringBuffer upperName = new StringBuffer(name.length()); + + for (int i = 0; i < name.length(); i++) + { + char ch = name.charAt(i); + if (ch >= 'a' && ch <= 'z') + upperName.append(Character.toUpperCase(ch)); + else + upperName.append(ch); + } + + // Return the uppercased name + + return upperName.toString(); + } + + // Invalid or empty name + + return ""; + } + + /** + * Determine if the name conversion character set has been configured + * + * @return boolean + */ + public final static boolean hasNameConversionCharacterSet() + { + return _nameConversionCharset != null ? true : false; + } + + /** + * Return the name conversion character set name + * + * @return String + */ + public final static String getNameConversionCharacterSet() + { + return _nameConversionCharset; + } + + /** + * Set the name conversion character set + * + * @param charSet String + */ + public final static void setNameConversionCharacterSet(String charSet) + { + _nameConversionCharset = charSet; + } + + /** + * Return the NetBIOS name as a string. + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + str.append(m_name); + + if (hasNameScope()) + { + str.append("."); + str.append(m_scope); + } + + str.append(":"); + str.append(TypeAsString(m_type)); + str.append(","); + if (m_group == true) + str.append("Group,"); + else + str.append("Unique,"); + if (numberOfAddresses() > 0) + { + for (int i = 0; i < numberOfAddresses(); i++) + { + str.append(getIPAddressString(i)); + str.append("|"); + } + } + str.append("]"); + return str.toString(); + } + + /** + * Convert a the NetBIOS name into RFC NetBIOS format. + * + * @return byte[] + */ + public byte[] encodeName() + { + + // Build the name string with the name type, make sure that the host + // name is uppercase. + + StringBuffer nbName = new StringBuffer(getName().toUpperCase()); + + if (nbName.length() > NameLength - 1) + nbName.setLength(NameLength - 1); + + // Space pad the name then add the NetBIOS name type + + while (nbName.length() < NameLength - 1) + nbName.append(' '); + nbName.append(getType()); + + // Allocate the return buffer. + // + // Length byte + encoded NetBIOS name length + name scope length + name scope + + int len = 34; + if (hasNameScope()) + len += getNameScope().length() + 1; + + byte[] encBuf = new byte[len]; + + // Convert the NetBIOS name string to the RFC NetBIOS name format + + int pos = 0; + encBuf[pos++] = (byte) 32; + int idx = 0; + + while (idx < nbName.length()) + { + + // Get the current character from the host name string + + char ch = nbName.charAt(idx++); + + if (ch == ' ') + { + + // Append an encoded character + + encBuf[pos++] = (byte) 'C'; + encBuf[pos++] = (byte) 'A'; + } + else + { + + // Append octet for the current character + + encBuf[pos++] = (byte) EncodeConversion.charAt((int) ch / 16); + encBuf[pos++] = (byte) EncodeConversion.charAt((int) ch % 16); + } + } + + // Check if there is a NetBIOS name scope to be appended to the encoded name string + + if (hasNameScope()) + { + + // Get the name scope and uppercase + + StringTokenizer tokens = new StringTokenizer(getNameScope(), "."); + + while (tokens.hasMoreTokens()) + { + + // Get the current token + + String token = tokens.nextToken(); + + // Append the name to the encoded NetBIOS name + + encBuf[pos++] = (byte) token.length(); + for (int i = 0; i < token.length(); i++) + encBuf[pos++] = (byte) token.charAt(i); + } + } + + // Terminate the encoded name string with a null section length + + encBuf[pos++] = (byte) 0; + + // Return the encoded NetBIOS name + + return encBuf; + } + + /** + * Find the best match address that the NetBIOS name is registered on that matches one of the + * local TCP/IP addresses + * + * @param addrList InetAddress[] + * @return int + */ + public final int findBestMatchAddress(InetAddress[] addrList) + { + + // Check if the address list is valid + + if (addrList == null || addrList.length == 0 || numberOfAddresses() == 0) + return -1; + + // If the NetBIOS name only has one address then just return the index + + if (numberOfAddresses() == 1) + return 0; + + // Search for a matching subnet + + int topCnt = 0; + int topIdx = -1; + + for (int localIdx = 0; localIdx < addrList.length; localIdx++) + { + + // Get the address bytes for the current local address + + byte[] localAddr = addrList[localIdx].getAddress(); + + // Match against the addresses that the NetBIOS name is registered against + + for (int addrIdx = 0; addrIdx < numberOfAddresses(); addrIdx++) + { + + // Get the current remote address bytes + + byte[] remoteAddr = (byte[]) m_addrList.elementAt(addrIdx); + int ipIdx = 0; + + while (ipIdx < 4 && remoteAddr[ipIdx] == localAddr[ipIdx]) + ipIdx++; + + // Check if the current address is the best match so far + + if (ipIdx > topIdx) + { + + // Update the best match address + + topIdx = addrIdx; + topCnt = ipIdx; + } + } + } + + // Return the best match index, or -1 if no match found + + return topIdx; + } + + /** + * Decode a NetBIOS name string and create a new NetBIOSName object + * + * @param buf byte[] + * @param off int + * @return NetBIOSName + */ + public static NetBIOSName decodeNetBIOSName(byte[] buf, int off) + { + + // Convert the RFC NetBIOS name string to a normal NetBIOS name string + + StringBuffer nameBuf = new StringBuffer(16); + + int nameLen = (int) buf[off++]; + int idx = 0; + char ch1, ch2; + + while (idx < nameLen) + { + + // Get the current encoded character pair from the encoded name string + + ch1 = (char) buf[off++]; + ch2 = (char) buf[off++]; + + if (ch1 == 'C' && ch2 == 'A') + { + + // Append a character + + nameBuf.append(' '); + } + else + { + + // Convert back to a character code + + int val = EncodeConversion.indexOf(ch1) << 4; + val += EncodeConversion.indexOf(ch2); + + // Append the current character to the decoded name + + nameBuf.append((char) (val & 0xFF)); + } + + // Update the encoded string index + + idx += 2; + + } + + // Decode the NetBIOS name scope, if specified + + StringBuffer scopeBuf = new StringBuffer(128); + nameLen = (int) buf[off++]; + + while (nameLen > 0) + { + + // Append a name seperator if not the first name section + + if (scopeBuf.length() > 0) + scopeBuf.append("."); + + // Copy the name scope section to the scope name buffer + + for (int i = 0; i < nameLen; i++) + scopeBuf.append((char) buf[off++]); + + // Get the next name section length + + nameLen = (int) buf[off++]; + } + + // Create a NetBIOS name + + return new NetBIOSName(nameBuf.toString(), scopeBuf.toString()); + } + + /** + * Decode a NetBIOS name string length + * + * @param buf byte[] + * @param off int + * @return int + */ + public static int decodeNetBIOSNameLength(byte[] buf, int off) + { + + // Calculate the encoded NetBIOS name string length + + int totLen = 1; + int nameLen = (int) buf[off++]; + + while (nameLen > 0) + { + + // Update the total encoded name length + + totLen += nameLen; + off += nameLen; + + // Get the next name section length + + nameLen = (int) buf[off++]; + totLen++; + } + + // Return the encoded NetBIOS name length + + return totLen; + } + + /** + * Return the NetBIOS name type as a string. + * + * @param typ char + * @return String + */ + public final static String TypeAsString(char typ) + { + + // Return the NetBIOS name type string + + String nameTyp = ""; + + switch (typ) + { + case WorkStation: + nameTyp = "WorkStation"; + break; + case Messenger: + nameTyp = "Messenger"; + break; + case RemoteMessenger: + nameTyp = "RemoteMessenger"; + break; + case RASServer: + nameTyp = "RASServer"; + break; + case FileServer: + nameTyp = "FileServer"; + break; + case RASClientService: + nameTyp = "RASClientService"; + break; + case MSExchangeInterchange: + nameTyp = "MSExchangeInterchange"; + break; + case MSExchangeStore: + nameTyp = "MSExchangeStore"; + break; + case MSExchangeDirectory: + nameTyp = "MSExchangeDirectory"; + break; + case LotusNotesServerService: + nameTyp = "LotusNotesServerService"; + break; + case ModemSharingService: + nameTyp = "ModemSharingService"; + break; + case ModemSharingClient: + nameTyp = "ModemSharingClient"; + break; + case McCaffeeAntiVirus: + nameTyp = "McCaffeeAntiVirus"; + break; + case SMSClientRemoteControl: + nameTyp = "SMSClientRemoteControl"; + break; + case SMSAdminRemoteControl: + nameTyp = "SMSAdminRemoteControl"; + break; + case SMSClientRemoteChat: + nameTyp = "SMSClientRemoteChat"; + break; + case SMSClientRemoteTransfer: + nameTyp = "SMSClientRemoteTransfer"; + break; + case DECPathworksService: + nameTyp = "DECPathworksService"; + break; + case MSExchangeIMC: + nameTyp = "MSExchangeIMC"; + break; + case MSExchangeMTA: + nameTyp = "MSExchangeMTA"; + break; + case NetworkMonitorAgent: + nameTyp = "NetworkMonitorAgent"; + break; + case NetworkMonitorApp: + nameTyp = "NetworkMonitorApp"; + break; + case DomainMasterBrowser: + nameTyp = "DomainMasterBrowser"; + break; + case MasterBrowser: + nameTyp = "MasterBrowser"; + break; + case DomainAnnounce: + nameTyp = "DomainAnnounce"; + break; + case DomainControllers: + nameTyp = "DomainControllers"; + break; + default: + nameTyp = "0x" + Integer.toHexString((int) typ); + break; + } + + return nameTyp; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSNameList.java b/source/java/org/alfresco/filesys/netbios/NetBIOSNameList.java new file mode 100644 index 0000000000..dbe5e0a267 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSNameList.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import java.util.Vector; + +/** + * NetBIOS Name List Class + */ +public class NetBIOSNameList +{ + + // List of NetBIOS names + + private Vector m_nameList; + + /** + * Class constructor + */ + public NetBIOSNameList() + { + m_nameList = new Vector(); + } + + /** + * Add a name to the list + * + * @param name NetBIOSName + */ + public final void addName(NetBIOSName name) + { + m_nameList.add(name); + } + + /** + * Get a name from the list + * + * @param idx int + * @return NetBIOSName + */ + public final NetBIOSName getName(int idx) + { + if (idx < m_nameList.size()) + return m_nameList.get(idx); + return null; + } + + /** + * Return the number of names in the list + * + * @return int + */ + public final int numberOfNames() + { + return m_nameList.size(); + } + + /** + * Find names of the specified name of different types and return a subset of the available + * names. + * + * @param name String + * @return NetBIOSNameList + */ + public final NetBIOSNameList findNames(String name) + { + + // Allocate the sub list and search for required names + + NetBIOSNameList subList = new NetBIOSNameList(); + for (int i = 0; i < m_nameList.size(); i++) + { + NetBIOSName nbName = getName(i); + if (nbName.getName().compareTo(name) == 0) + subList.addName(nbName); + } + + // Return the sub list of names + + return subList; + } + + /** + * Find the first name of the specified type + * + * @param typ char + * @param group boolean + * @return NetBIOSName + */ + public final NetBIOSName findName(char typ, boolean group) + { + + // Search for the first name of the required type + + for (int i = 0; i < m_nameList.size(); i++) + { + NetBIOSName name = getName(i); + if (name.getType() == typ && name.isGroupName() == group) + return name; + } + + // Name type not found + + return null; + } + + /** + * Find the specified name and type + * + * @param name String + * @param typ char + * @param group boolean + * @return NetBIOSName + */ + public final NetBIOSName findName(String name, char typ, boolean group) + { + + // Search for the first name of the required type + + for (int i = 0; i < m_nameList.size(); i++) + { + NetBIOSName nbName = getName(i); + if (nbName.getName().equals(name) && nbName.getType() == typ && nbName.isGroupName() == group) + return nbName; + } + + // Name/type not found + + return null; + } + + /** + * Find names of the specified type and return a subset of the available names + * + * @param typ char + * @param group boolean + * @return NetBIOSNameList + */ + public final NetBIOSNameList findNames(char typ, boolean group) + { + + // Allocate the sub list and search for names of the required type + + NetBIOSNameList subList = new NetBIOSNameList(); + for (int i = 0; i < m_nameList.size(); i++) + { + NetBIOSName name = getName(i); + if (name.getType() == typ && name.isGroupName() == group) + subList.addName(name); + } + + // Return the sub list of names + + return subList; + } + + /** + * Remove a name from the list + * + * @param name NetBIOSName + * @return NetBIOSName + */ + public final NetBIOSName removeName(NetBIOSName name) + { + for (int i = 0; i < m_nameList.size(); i++) + { + NetBIOSName curName = getName(i); + if (curName.equals(name)) + { + m_nameList.removeElementAt(i); + return curName; + } + } + return null; + } + + /** + * Delete all names from the list + */ + public final void removeAllNames() + { + m_nameList.removeAllElements(); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSPacket.java b/source/java/org/alfresco/filesys/netbios/NetBIOSPacket.java new file mode 100644 index 0000000000..97477d129b --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSPacket.java @@ -0,0 +1,1247 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.HexDump; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * NetBIOS Packet Class + */ +public class NetBIOSPacket +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol.netbios"); + + // Minimum valid receive length + + public static final int MIN_RXLEN = 4; + + // NetBIOS opcodes + + public static final int NAME_QUERY = 0x00; + public static final int NAME_REGISTER = 0x05; + public static final int NAME_RELEASE = 0x06; + public static final int WACK = 0x07; + public static final int REFRESH = 0x08; + public static final int NAME_REGISTER_MULTI = 0x0F; + + public static final int RESP_QUERY = 0x10; + public static final int RESP_REGISTER = 0x15; + public static final int RESP_RELEASE = 0x16; + + // NetBIOS opcode masks + + public static final int MASK_OPCODE = 0xF800; + public static final int MASK_NMFLAGS = 0x07F0; + public static final int MASK_RCODE = 0x000F; + + public static final int MASK_NOOPCODE = 0x07FF; + public static final int MASK_NOFLAGS = 0xF80F; + public static final int MASK_NORCODE = 0xFFF0; + + public static final int MASK_RESPONSE = 0x0010; + + // Flags bit values + + public static final int FLG_BROADCAST = 0x0001; + public static final int FLG_RECURSION = 0x0008; + public static final int FLG_RECURSDES = 0x0010; + public static final int FLG_TRUNCATION = 0x0020; + public static final int FLG_AUTHANSWER = 0x0040; + + // NetBIOS name lookup types + + public static final int NAME_TYPE_NB = 0x0020; + public static final int NAME_TYPE_NBSTAT = 0x0021; + + // RFC NetBIOS encoded name length + + public static final int NAME_LEN = 32; + + // NetBIOS name classes + + public static final int NAME_CLASS_IN = 0x0001; + + // Bit shifts for opcode/flags values + + private static final int SHIFT_FLAGS = 4; + private static final int SHIFT_OPCODE = 11; + + // Default NetBIOS buffer size to allocate + + public static final int DEFAULT_BUFSIZE = 1024; + + // NetBIOS packet offsets + + private static final int NB_TRANSID = 0; + private static final int NB_OPCODE = 2; + private static final int NB_QDCOUNT = 4; + private static final int NB_ANCOUNT = 6; + private static final int NB_NSCOUNT = 8; + private static final int NB_ARCOUNT = 10; + private static final int NB_DATA = 12; + + // NetBIOS name registration error reponse codes (RCODE field) + + public static final int FMT_ERR = 0x01; + public static final int SRV_ERR = 0x02; + public static final int IMP_ERR = 0x04; + public static final int RFS_ERR = 0x05; + public static final int ACT_ERR = 0x06; + public static final int CFT_ERR = 0x07; + + // Name flags + + public static final int NAME_PERM = 0x02; + public static final int NAME_ACTIVE = 0x04; + public static final int NAME_CONFLICT = 0x08; + public static final int NAME_DEREG = 0x10; + public static final int NAME_GROUP = 0x80; + + // NetBIOS packet buffer + + private byte[] m_nbbuf; + + // Actual used packet length + + private int m_datalen; + + /** + * Default constructor + */ + public NetBIOSPacket() + { + m_nbbuf = new byte[DEFAULT_BUFSIZE]; + m_datalen = NB_DATA; + } + + /** + * Create a NetBIOS packet with the specified buffer. + * + * @param buf byte[] + */ + public NetBIOSPacket(byte[] buf) + { + m_nbbuf = buf; + m_datalen = NB_DATA; + } + + /** + * Create a NetBIOS packet with the specified buffer size. + * + * @param siz int + */ + public NetBIOSPacket(int siz) + { + m_nbbuf = new byte[siz]; + m_datalen = NB_DATA; + } + + /** + * Dump the packet structure to the console. + * + * @param sessPkt True if this is a NetBIOS session packet, else false. + */ + public void DumpPacket(boolean sessPkt) + { + + // Display the transaction id + + logger.debug("NetBIOS Packet Dump :-"); + + // Detrmine the packet type + + if (sessPkt == true) + { + + switch (getPacketType()) + { + + // NetBIOS session request + + case RFCNetBIOSProtocol.SESSION_REQUEST: + StringBuffer name = new StringBuffer(); + for (int i = 0; i < 32; i++) + name.append((char) m_nbbuf[39 + i]); + logger.debug("Session request from " + NetBIOSSession.DecodeName(name.toString())); + break; + + // NetBIOS message + + case RFCNetBIOSProtocol.SESSION_MESSAGE: + break; + } + } + else + { + + // Display the packet type + + logger.debug(" Transaction Id : " + getTransactionId()); + + String opCode = null; + + switch (getOpcode()) + { + case NAME_QUERY: + opCode = "QUERY"; + break; + case RESP_QUERY: + opCode = "QUERY (Response)"; + break; + case NAME_REGISTER: + opCode = "NAME REGISTER"; + break; + case RESP_REGISTER: + opCode = "NAME REGISTER (Response)"; + break; + case NAME_RELEASE: + opCode = "NAME RELEASE"; + break; + case RESP_RELEASE: + opCode = "NAME RELEASE (Response)"; + break; + case WACK: + opCode = "WACK"; + break; + case REFRESH: + opCode = "REFRESH"; + break; + default: + opCode = Integer.toHexString(getOpcode()); + break; + } + logger.debug(" Opcode : " + opCode); + + // Display the flags + + logger.debug(" Flags : " + Integer.toHexString(getFlags())); + + // Display the name counts + + logger.debug(" QDCount : " + getQuestionCount()); + logger.debug(" ANCount : " + getAnswerCount()); + logger.debug(" NSCount : " + getNameServiceCount()); + logger.debug(" ARCount : " + getAdditionalCount()); + + // Dump the question name, if there is one + + if (getQuestionCount() > 0) + { + + // Get the encoded name string + + StringBuffer encName = new StringBuffer(); + for (int i = 1; i <= 32; i++) + encName.append((char) m_nbbuf[NB_DATA + i]); + + // Decode the name + + String name = NetBIOSSession.DecodeName(encName.toString()); + logger.debug(" QName : " + name + " <" + NetBIOSName.TypeAsString(name.charAt(15)) + ">"); + } + } + + // Dump the raw data + + logger.debug("********** Raw NetBIOS Data Dump **********"); + HexDump.Dump(getBuffer(), getLength(), 0); + } + + /** + * Get the additional byte count. + * + * @return int + */ + public final int getAdditionalCount() + { + return DataPacker.getShort(m_nbbuf, NB_ARCOUNT); + } + + /** + * Get the answer name details + * + * @return String + */ + public final String getAnswerName() + { + + // Pack the encoded name into the NetBIOS packet + + return NetBIOSSession.DecodeName(m_nbbuf, NB_DATA + 1); + } + + /** + * Get the answer count. + * + * @return int + */ + public final int getAnswerCount() + { + return DataPacker.getShort(m_nbbuf, NB_ANCOUNT); + } + + /** + * Get the answer name list + * + * @return NetBIOSNameList + */ + public final NetBIOSNameList getAnswerNameList() + { + + // Check if there are any answer names + + int cnt = getAnswerCount(); + if (cnt == 0) + return null; + + NetBIOSNameList nameList = new NetBIOSNameList(); + int pos = NB_DATA; + + while (cnt-- > 0) + { + + // Get a NetBIOS name from the buffer + + int nameLen = NetBIOSName.decodeNetBIOSNameLength(m_nbbuf, pos); + NetBIOSName name = NetBIOSName.decodeNetBIOSName(m_nbbuf, pos); + + // Skip the type, class and TTL + + pos += nameLen; + pos += 8; + + // Get the count of data bytes + + int dataCnt = DataPacker.getShort(m_nbbuf, pos); + pos += 2; + + while (dataCnt > 0) + { + + // Get the flags, check if the name is a unique or group name + + int flags = DataPacker.getShort(m_nbbuf, pos); + pos += 2; + if ((flags & NAME_GROUP) != 0) + name.setGroup(true); + + // Get the IP address and add to the list of addresses for the current name + + byte[] ipaddr = new byte[4]; + for (int i = 0; i < 4; i++) + ipaddr[i] = m_nbbuf[pos++]; + + name.addIPAddress(ipaddr); + + // Update the data count + + dataCnt -= 6; + } + + // Add the name to the name list + + nameList.addName(name); + } + + // Return the name list + + return nameList; + } + + /** + * Get the answer name list from an adapter status reply + * + * @return NetBIOSNameList + */ + public final NetBIOSNameList getAdapterStatusNameList() + { + + // Check if there are any answer names + + int cnt = getAnswerCount(); + if (cnt == 0) + return null; + + NetBIOSNameList nameList = new NetBIOSNameList(); + int pos = NB_DATA; + + // Skip the initial name + + int nameLen = (int) (m_nbbuf[pos++] & 0xFF); + pos += nameLen; + pos = DataPacker.wordAlign(pos); + pos += 8; + + // Get the count of data bytes and name count + + int dataCnt = DataPacker.getShort(m_nbbuf, pos); + pos += 2; + + int nameCnt = (int) (m_nbbuf[pos++] & 0xFF); + + while (nameCnt > 0 && dataCnt > 0) + { + + // Get the NetBIOS name/type + + NetBIOSName nbName = new NetBIOSName(m_nbbuf, pos); + pos += 16; + + // Get the name type flags, check if this is a unique or group name + + int typ = DataPacker.getShort(m_nbbuf, pos); + pos += 2; + + if ((typ & NAME_GROUP) != 0) + nbName.setGroup(true); + + // Add the name to the list + + nameList.addName(nbName); + + // Update the data count and name count + + dataCnt -= 18; + nameCnt--; + } + + // Return the name list + + return nameList; + } + + /** + * Return the NetBIOS buffer. + * + * @return byte[] + */ + public final byte[] getBuffer() + { + return m_nbbuf; + } + + /** + * Get the flags from the received NetBIOS packet. + * + * @return int + */ + public final int getFlags() + { + int flags = DataPacker.getShort(m_nbbuf, NB_QDCOUNT) & MASK_NMFLAGS; + flags = flags >> SHIFT_FLAGS; + return flags; + } + + /** + * Return the NetBIOS header flags value. + * + * @return int + */ + public final int getHeaderFlags() + { + return m_nbbuf[1] & 0x00FF; + } + + /** + * Return the NetBIOS header data length value. + * + * @return int + */ + public final int getHeaderLength() + { + return DataPacker.getIntelShort(m_nbbuf, 2) & 0xFFFF; + } + + /** + * Return the NetBIOS header message type. + * + * @return int + */ + public final int getHeaderType() + { + return m_nbbuf[0] & 0x00FF; + } + + /** + * Return the received packet length. + * + * @return int + */ + public final int getLength() + { + return m_datalen; + } + + /** + * Return the name service count. + * + * @return int + */ + public final int getNameServiceCount() + { + return DataPacker.getShort(m_nbbuf, NB_NSCOUNT); + } + + /** + * Return the NetBIOS opcode. + * + * @return int + */ + public final int getOpcode() + { + int op = DataPacker.getShort(m_nbbuf, NB_OPCODE) & MASK_OPCODE; + op = op >> SHIFT_OPCODE; + return op; + } + + /** + * Return the NetBIOS packet type. + * + * @return int + */ + public final int getPacketType() + { + return (int) (m_nbbuf[0] & 0xFF); + } + + /** + * Return the question count. + * + * @return int + */ + public final int getQuestionCount() + { + return DataPacker.getShort(m_nbbuf, NB_QDCOUNT); + } + + /** + * Get the question name. + */ + public final String getQuestionName() + { + + // Pack the encoded name into the NetBIOS packet + + return NetBIOSSession.DecodeName(m_nbbuf, NB_DATA + 1); + } + + /** + * Get the question name length. + */ + public final int getQuestionNameLength() + { + + // Pack the encoded name into the NetBIOS packet + + return (int) m_nbbuf[NB_DATA] & 0xFF; + } + + /** + * Return the result code for the received packet. + * + * @return int + */ + public final int getResultCode() + { + int res = DataPacker.getShort(m_nbbuf, NB_OPCODE) & MASK_RCODE; + return res; + } + + /** + * Return the NetBIOS transaction id. + * + * @return int + */ + public final int getTransactionId() + { + return DataPacker.getShort(m_nbbuf, NB_TRANSID); + } + + /** + * Determine if the received packet is a repsonse packet. + * + * @return boolean + */ + public final boolean isResponse() + { + if ((getOpcode() & MASK_RESPONSE) != 0) + return true; + return false; + } + + /** + * Set the additional byte count. + * + * @param cnt int + */ + public final void setAdditionalCount(int cnt) + { + DataPacker.putShort((short) cnt, m_nbbuf, NB_ARCOUNT); + } + + /** + * Set the answer byte count. + * + * @param cnt int + */ + public final void setAnswerCount(int cnt) + { + DataPacker.putShort((short) cnt, m_nbbuf, NB_ANCOUNT); + } + + /** + * Set the answer name. + * + * @param name java.lang.String + * @param qtyp int + * @param qcls int + */ + public final int setAnswerName(String name, char ntyp, int qtyp, int qcls) + { + + // RFC encode the NetBIOS name string + + String encName = NetBIOSSession.ConvertName(name, ntyp); + byte[] nameByts = encName.getBytes(); + + // Pack the encoded name into the NetBIOS packet + + int pos = NB_DATA; + m_nbbuf[pos++] = (byte) NAME_LEN; + + for (int i = 0; i < 32; i++) + m_nbbuf[pos++] = nameByts[i]; + m_nbbuf[pos++] = 0x00; + + // Set the name type and class + + DataPacker.putShort((short) qtyp, m_nbbuf, pos); + pos += 2; + + DataPacker.putShort((short) qcls, m_nbbuf, pos); + pos += 2; + + // Set the packet length + + if (pos > m_datalen) + setLength(pos); + return pos; + } + + /** + * Set the flags. + * + * @param flg int + */ + public final void setFlags(int flg) + { + int val = DataPacker.getShort(m_nbbuf, NB_OPCODE) & MASK_NOFLAGS; + val += (flg << SHIFT_FLAGS); + DataPacker.putShort((short) val, m_nbbuf, NB_OPCODE); + } + + /** + * Set the NetBIOS packet header flags value. + * + * @param flg int + */ + public final void setHeaderFlags(int flg) + { + m_nbbuf[1] = (byte) (flg & 0x00FF); + } + + /** + * Set the NetBIOS packet data length in the packet header. + * + * @param len int + */ + public final void setHeaderLength(int len) + { + DataPacker.putIntelShort(len, m_nbbuf, 2); + } + + /** + * Set the NetBIOS packet type in the packet header. + * + * @param typ int + */ + public final void setHeaderType(int typ) + { + m_nbbuf[0] = (byte) (typ & 0x00FF); + } + + /** + * Set the IP address. + * + * @return int + * @param off int + * @param ipaddr byte[] + */ + public final int setIPAddress(int off, byte[] ipaddr) + { + + // Pack the IP address + + for (int i = 0; i < 4; i++) + m_nbbuf[off + i] = ipaddr[i]; + + // Set the packet length + + int pos = off + 4; + if (pos > m_datalen) + setLength(pos); + + // Return the new packet offset + + return pos; + } + + /** + * Set the packet data length. + * + * @param len int + */ + public final void setLength(int len) + { + m_datalen = len; + } + + /** + * Set the name registration flags. + * + * @return int + * @param off int + * @param flg int + */ + public final int setNameRegistrationFlags(int off, int flg) + { + + // Set the name registration flags + + DataPacker.putShort((short) 0x0006, m_nbbuf, off); + DataPacker.putShort((short) flg, m_nbbuf, off + 2); + + // Set the packet length + + int pos = off + 4; + if (pos > m_datalen) + setLength(pos); + + // Return the new packet offset + + return pos; + } + + /** + * Set the name service count. + * + * @param cnt int + */ + public final void setNameServiceCount(int cnt) + { + DataPacker.putShort((short) cnt, m_nbbuf, NB_NSCOUNT); + } + + /** + * Set the NetBIOS opcode. + * + * @param op int + */ + public final void setOpcode(int op) + { + int val = DataPacker.getShort(m_nbbuf, NB_OPCODE) & MASK_NOOPCODE; + val = val + (op << SHIFT_OPCODE); + DataPacker.putShort((short) val, m_nbbuf, NB_OPCODE); + } + + /** + * Set the question count. + * + * @param cnt int + */ + public final void setQuestionCount(int cnt) + { + DataPacker.putShort((short) cnt, m_nbbuf, NB_QDCOUNT); + } + + /** + * Set the question name. + * + * @param name NetBIOSName + * @param qtyp int + * @param qcls int + * @return int + */ + public final int setQuestionName(NetBIOSName name, int qtyp, int qcls) + { + + // Encode the NetBIOS name + + byte[] nameByts = name.encodeName(); + + // Pack the encoded name into the NetBIOS packet + + int pos = NB_DATA; + System.arraycopy(nameByts, 0, m_nbbuf, pos, nameByts.length); + pos += nameByts.length; + + // Set the name type and class + + DataPacker.putShort((short) qtyp, m_nbbuf, pos); + pos += 2; + + DataPacker.putShort((short) qcls, m_nbbuf, pos); + pos += 2; + + // Set the packet length + + if (pos > m_datalen) + setLength(pos); + return pos; + } + + /** + * Set the question name. + * + * @param name java.lang.String + * @param qtyp int + * @param qcls int + */ + public final int setQuestionName(String name, char ntyp, int qtyp, int qcls) + { + + // RFC encode the NetBIOS name string + + String encName = NetBIOSSession.ConvertName(name, ntyp); + byte[] nameByts = encName.getBytes(); + + // Pack the encoded name into the NetBIOS packet + + int pos = NB_DATA; + m_nbbuf[pos++] = (byte) NAME_LEN; + + for (int i = 0; i < 32; i++) + m_nbbuf[pos++] = nameByts[i]; + m_nbbuf[pos++] = 0x00; + + // Set the name type and class + + DataPacker.putShort((short) qtyp, m_nbbuf, pos); + pos += 2; + + DataPacker.putShort((short) qcls, m_nbbuf, pos); + pos += 2; + + // Set the packet length + + if (pos > m_datalen) + setLength(pos); + return pos; + } + + /** + * Pack the resource data into the packet. + * + * @return int + * @param off int + * @param flg int + * @param data byte[] + * @param len int + */ + public final int setResourceData(int off, int flg, byte[] data, int len) + { + + // Set the resource data type + + DataPacker.putShort((short) flg, m_nbbuf, off); + + // Pack the data + + int pos = off + 2; + for (int i = 0; i < len; i++) + m_nbbuf[pos++] = data[i]; + + // Set the packet length + + if (pos > m_datalen) + setLength(pos); + return pos; + } + + /** + * Set the resource data length in the NetBIOS packet. + * + * @return int + * @param off int + * @param len int + */ + public final int setResourceDataLength(int off, int len) + { + + // Set the resource data length + + DataPacker.putShort((short) len, m_nbbuf, off); + + // Set the packet length + + int pos = off + 2; + if (pos > m_datalen) + setLength(pos); + + // Return the new packet offset + + return pos; + } + + /** + * Set the resource record. + * + * @param pktoff Packet offset to pack the resource record. + * @param offset Offset to name. + * @param qtyp int + * @param qcls int + */ + public final int setResourceRecord(int pktoff, int rroff, int qtyp, int qcls) + { + + // Pack the resource record details + + DataPacker.putShort((short) (0xC000 + rroff), m_nbbuf, pktoff); + DataPacker.putShort((short) qtyp, m_nbbuf, pktoff + 2); + DataPacker.putShort((short) qcls, m_nbbuf, pktoff + 4); + + // Set the packet length + + int pos = pktoff + 6; + if (pos > m_datalen) + setLength(pos); + + // Return the new packet offset + + return pos; + } + + /** + * Set the transaction id. + * + * @param id int + */ + public final void setTransactionId(int id) + { + DataPacker.putShort((short) id, m_nbbuf, NB_TRANSID); + } + + /** + * Set the time to live for the packet. + * + * @return int + * @param off int + * @param ttl int + */ + public final int setTTL(int off, int ttl) + { + + // Set the time to live value for the packet + + DataPacker.putInt(ttl, m_nbbuf, off); + + // Set the packet length + + int pos = off + 4; + if (pos > m_datalen) + setLength(pos); + + // Return the new packet offset + + return pos; + } + + /** + * Return a packet type as a string + * + * @param typ int + * @return String + */ + public final static String getTypeAsString(int typ) + { + + // Return the NetBIOS packet type as a string + + String typStr = ""; + + switch (typ) + { + case RFCNetBIOSProtocol.SESSION_ACK: + typStr = "SessionAck"; + break; + case RFCNetBIOSProtocol.SESSION_KEEPALIVE: + typStr = "SessionKeepAlive"; + break; + case RFCNetBIOSProtocol.SESSION_MESSAGE: + typStr = "SessionMessage"; + break; + case RFCNetBIOSProtocol.SESSION_REJECT: + typStr = "SessionReject"; + break; + case RFCNetBIOSProtocol.SESSION_REQUEST: + typStr = "SessionRequest"; + break; + case RFCNetBIOSProtocol.SESSION_RETARGET: + typStr = "SessionRetarget"; + break; + default: + typStr = "Unknown 0x" + Integer.toHexString(typ); + break; + } + + // Return the packet type string + + return typStr; + } + + /** + * Build a name query response packet for the specified NetBIOS name + * + * @param name NetBIOSName + * @return int + */ + public final int buildNameQueryResponse(NetBIOSName name) + { + + // Fill in the header + + setOpcode(NetBIOSPacket.RESP_QUERY); + setFlags(NetBIOSPacket.FLG_RECURSDES + NetBIOSPacket.FLG_AUTHANSWER); + + setQuestionCount(0); + setAnswerCount(1); + setAdditionalCount(0); + setNameServiceCount(0); + + int pos = setAnswerName(name.getName(), name.getType(), 0x20, 0x01); + pos = setTTL(pos, 10000); + pos = setResourceDataLength(pos, name.numberOfAddresses() * 6); + + // Pack the IP address(es) for this name + + for (int i = 0; i < name.numberOfAddresses(); i++) + { + + // Get the current IP address + + byte[] ipaddr = name.getIPAddress(i); + + // Pack the NetBIOS flags and IP address + + DataPacker.putShort((short) 0x00, m_nbbuf, pos); + pos += 2; + + for (int j = 0; j < 4; j++) + m_nbbuf[pos++] = ipaddr[j]; + } + + // Set the packet length, and return the length + + setLength(pos); + return getLength(); + } + + /** + * Build an add name request packet for the specified NetBIOS name + * + * @param name NetBIOSName + * @param addrIdx int + * @param tranId int + * @return int + */ + public final int buildAddNameRequest(NetBIOSName name, int addrIdx, int tranId) + { + + // Initialize an add name NetBIOS packet + + setTransactionId(tranId); + setOpcode(NetBIOSPacket.NAME_REGISTER); + setFlags(NetBIOSPacket.FLG_BROADCAST + NetBIOSPacket.FLG_RECURSION); + + setQuestionCount(1); + setAnswerCount(0); + setNameServiceCount(0); + setAdditionalCount(1); + + int pos = setQuestionName(name.getName(), name.getType(), 0x20, 0x01); + pos = setResourceRecord(pos, 12, 0x20, 0x01); + + if (name.getTimeToLive() == 0) + pos = setTTL(pos, NetBIOSName.DefaultTTL); + else + pos = setTTL(pos, name.getTimeToLive()); + + short flg = 0; + if (name.isGroupName()) + flg = (short) 0x8000; + pos = setNameRegistrationFlags(pos, flg); + pos = setIPAddress(pos, name.getIPAddress(addrIdx)); + + // Return the packet length + + setLength(pos); + return pos; + } + + /** + * Build a refresh name request packet for the specified NetBIOS name + * + * @param name NetBIOSName + * @param addrIdx int + * @param tranId int + * @return int + */ + public final int buildRefreshNameRequest(NetBIOSName name, int addrIdx, int tranId) + { + + // Initialize an add name NetBIOS packet + + setTransactionId(tranId); + setOpcode(NetBIOSPacket.REFRESH); + setFlags(NetBIOSPacket.FLG_BROADCAST + NetBIOSPacket.FLG_RECURSION); + + setQuestionCount(1); + setAnswerCount(0); + setNameServiceCount(0); + setAdditionalCount(1); + + int pos = setQuestionName(name.getName(), name.getType(), 0x20, 0x01); + pos = setResourceRecord(pos, 12, 0x20, 0x01); + + if (name.getTimeToLive() == 0) + pos = setTTL(pos, NetBIOSName.DefaultTTL); + else + pos = setTTL(pos, name.getTimeToLive()); + + short flg = 0; + if (name.isGroupName()) + flg = (short) 0x8000; + pos = setNameRegistrationFlags(pos, flg); + pos = setIPAddress(pos, name.getIPAddress(addrIdx)); + + // Return the packet length + + setLength(pos); + return pos; + } + + /** + * Build a delete name request packet for the specified NetBIOS name + * + * @param name NetBIOSName + * @param addrIdx int + * @param tranId int + * @return int + */ + public final int buildDeleteNameRequest(NetBIOSName name, int addrIdx, int tranId) + { + + // Initialize a delete name NetBIOS packet + + setTransactionId(tranId); + setOpcode(NetBIOSPacket.NAME_RELEASE); + setFlags(NetBIOSPacket.FLG_BROADCAST + NetBIOSPacket.FLG_RECURSION); + + setQuestionCount(1); + setAnswerCount(0); + setNameServiceCount(0); + setAdditionalCount(1); + + int pos = setQuestionName(name.getName(), name.getType(), 0x20, 0x01); + pos = setResourceRecord(pos, 12, 0x20, 0x01); + pos = setTTL(pos, 30000); + + short flg = 0; + if (name.isGroupName()) + flg = (short) 0x8000; + pos = setNameRegistrationFlags(pos, flg); + pos = setIPAddress(pos, name.getIPAddress(addrIdx)); + + // Return the packet length + + setLength(pos); + return pos; + } + + /** + * Build a name quesy request packet for the specified NetBIOS name + * + * @param name NetBIOSName + * @param tranId int + * @return int + */ + public final int buildNameQueryRequest(NetBIOSName name, int tranId) + { + + // Initialize a name query NetBIOS packet + + setTransactionId(tranId); + setOpcode(NetBIOSPacket.NAME_QUERY); + setFlags(NetBIOSSession.hasWINSServer() ? 0 : NetBIOSPacket.FLG_BROADCAST); + setQuestionCount(1); + return setQuestionName(name, NetBIOSPacket.NAME_TYPE_NB, NetBIOSPacket.NAME_CLASS_IN); + } + + /** + * Build a session setup request packet + * + * @param fromName NetBIOSName + * @param toName NetBIOSName + * @return int + */ + public final int buildSessionSetupRequest(NetBIOSName fromName, NetBIOSName toName) + { + + // Initialize the session setup packet header + + m_nbbuf[0] = (byte) RFCNetBIOSProtocol.SESSION_REQUEST; + m_nbbuf[1] = (byte) 0; // flags + + // Set the remote NetBIOS name + + int pos = 4; + byte[] encToName = toName.encodeName(); + System.arraycopy(encToName, 0, m_nbbuf, pos, encToName.length); + pos += encToName.length; + + // Set the local NetBIOS name + + byte[] encFromName = fromName.encodeName(); + System.arraycopy(encFromName, 0, m_nbbuf, pos, encFromName.length); + pos += encFromName.length; + + // Set the packet length + + setLength(pos); + + // Set the length in the session request header + + DataPacker.putShort((short) (pos - RFCNetBIOSProtocol.HEADER_LEN), m_nbbuf, 2); + + // Return the packet length + + return pos; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/NetBIOSSession.java b/source/java/org/alfresco/filesys/netbios/NetBIOSSession.java new file mode 100644 index 0000000000..ab8902a327 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetBIOSSession.java @@ -0,0 +1,1938 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Vector; + +import org.alfresco.filesys.smb.NetworkSession; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.HexDump; +import org.alfresco.filesys.util.StringList; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * NetBIOS session class. + */ +public final class NetBIOSSession implements NetworkSession +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol.netbios"); + + // Constants + // + // Caller name template + + public static final int MaxCallerNameTemplateLength = 8; + public static final char SessionIdChar = '#'; + public static final char JVMIdChar = '@'; + public static final String ValidTemplateChars = "@#_"; + + // Default find name buffer size + + private static final int FindNameBufferSize = 2048; + + // Default socket timeout, in milliseconds + + private static int _defTimeout = RFCNetBIOSProtocol.TMO; + + // Remote socket to connect to, default is 139. + + private int m_remotePort; + + // Socket used to connect and read/write to remote host + + private Socket m_nbSocket; + + // Input and output data streams, from the socket network connection + + private DataInputStream m_nbIn; + private DataOutputStream m_nbOut; + + // Send/receive timeout, in milliseconds + + private int m_tmo = _defTimeout; + + // Local and remote name types + + private char m_locNameType = NetBIOSName.FileServer; + private char m_remNameType = NetBIOSName.FileServer; + + // Unique session identifier, used to generate a unique caller name when opening a new session + + private static int m_sessIdx = 0; + + // Unique JVM id, used to generate a unique caller name when multiple JVMs may be running on the + // same + // host + + private static int m_jvmIdx = 0; + + // Caller name template string. The template is used to create a unique caller name when opening + // a new session. + // The template is appended to the local host name, which may be truncated to allow room for the + // template to be + // appended and still be within the 16 character NetBIOS name limit. + // + // The caller name generation replaces '#' characters with a zero padded session index as a hex + // value and '@' + // characters with a zero padded JVM index. Multiple '#' and/or '@' characters can be specified + // to indicate the + // field width. Any other characters in the template are passed through to the final caller name + // string. + // + // The maximum template string length is 8 characters to allow for at least 8 characters from + // the host name. + + private static String m_callerTemplate = "_##"; + + // Truncated host name, caller name generation appends the caller template result to this string + + private static String m_localNamePart; + + // Transaction identifier, used for datagrams + + private static short m_tranIdx = 1; + + // RFC NetBIOS name service datagram socket + + private static DatagramSocket m_dgramSock = null; + + // Debug enable flag + + private static boolean m_debug = false; + + // Subnet mask, required for broadcast name lookup requests + + private static String m_subnetMask = null; + + // WINS server address + + private static InetAddress m_winsServer; + + // Name lookup types + + public static final int DNSOnly = 1; + public static final int WINSOnly = 2; + public static final int WINSAndDNS = 3; + + // Flag to control whether name lookups use WINS/NetBIOS lookup or DNS + + private static int m_lookupType = WINSAndDNS; + + // NetBIOS name lookup timeout value. + + private static int m_lookupTmo = 500; + + // Flag to control use of the '*SMBSERVER' name when connecting to a file server + + private static boolean m_useWildcardFileServer = true; + + /** + * NetBIOS session class constructor. Create a NetBIOS session with the default socket number + * and no current network connection. + */ + public NetBIOSSession() + { + m_remotePort = RFCNetBIOSProtocol.PORT; + m_nbSocket = null; + } + + /** + * NetBIOS session class constructor + * + * @param tmo Send/receive timeout value in milliseconds + */ + public NetBIOSSession(int tmo) + { + m_tmo = tmo; + m_remotePort = RFCNetBIOSProtocol.PORT; + m_nbSocket = null; + } + + /** + * NetBIOS session class constructor + * + * @param tmo Send/receive timeout value in milliseconds + * @param port Remote port to connect to + */ + public NetBIOSSession(int tmo, int port) + { + m_tmo = tmo; + m_remotePort = port; + m_nbSocket = null; + } + + /** + * Return the protocol name + * + * @return String + */ + public final String getProtocolName() + { + return "TCP/IP NetBIOS"; + } + + /** + * Determine if the session is connected to a remote host + * + * @return boolean + */ + public final boolean isConnected() + { + + // Check if the socket is valid + + if (m_nbSocket == null) + return false; + return true; + } + + /** + * Check if there is data available on this network session + * + * @return boolean + * @exception IOException + */ + public final boolean hasData() throws IOException + { + + // Check if the connection is active + + if (m_nbSocket == null || m_nbIn == null) + return false; + + // Check if there is data available + + return m_nbIn.available() > 0 ? true : false; + } + + /** + * Convert a host name string into RFC NetBIOS format. + * + * @param hostName Host name to be converted. + * @return Converted host name string. + */ + public static String ConvertName(String hostName) + { + return ConvertName(hostName, NetBIOSName.FileServer); + } + + /** + * Convert a host name string into RFC NetBIOS format. + * + * @param hostName Host name to be converted. + * @param nameType NetBIOS name type, added as the 16th byte of the name before conversion. + * @return Converted host name string. + */ + public static String ConvertName(String hostName, char nameType) + { + + // Build the name string with the name type, make sure that the host + // name is uppercase. + + StringBuffer hName = new StringBuffer(hostName.toUpperCase()); + + if (hName.length() > 15) + hName.setLength(15); + + // Space pad the name then add the NetBIOS name type + + while (hName.length() < 15) + hName.append(' '); + hName.append(nameType); + + // Convert the NetBIOS name string to the RFC NetBIOS name format + + String convstr = new String("ABCDEFGHIJKLMNOP"); + StringBuffer nameBuf = new StringBuffer(32); + + int idx = 0; + + while (idx < hName.length()) + { + + // Get the current character from the host name string + + char ch = hName.charAt(idx++); + + if (ch == ' ') + { + + // Append an encoded character + + nameBuf.append("CA"); + } + else + { + + // Append octet for the current character + + nameBuf.append(convstr.charAt((int) ch / 16)); + nameBuf.append(convstr.charAt((int) ch % 16)); + } + + } // end while + + // Return the encoded string + + return nameBuf.toString(); + } + + /** + * Convert an encoded NetBIOS name to a normal name string + * + * @param buf Buffer that contains the NetBIOS encoded name + * @param off Offset that the name starts within the buffer + * @return Normal NetBIOS name string + */ + public static String DecodeName(byte[] buf, int off) + { + + // Convert the RFC NetBIOS name string to a normal NetBIOS name string + + String convstr = new String("ABCDEFGHIJKLMNOP"); + StringBuffer nameBuf = new StringBuffer(16); + + int idx = 0; + char ch1, ch2; + + while (idx < 32) + { + + // Get the current encoded character pair from the encoded name string + + ch1 = (char) buf[off + idx]; + ch2 = (char) buf[off + idx + 1]; + + if (ch1 == 'C' && ch2 == 'A') + { + + // Append a character + + nameBuf.append(' '); + } + else + { + + // Convert back to a character code + + int val = convstr.indexOf(ch1) << 4; + val += convstr.indexOf(ch2); + + // Append the current character to the decoded name + + nameBuf.append((char) (val & 0xFF)); + } + + // Update the encoded string index + + idx += 2; + + } // end while + + // Return the decoded string + + return nameBuf.toString(); + } + + /** + * Convert an encoded NetBIOS name to a normal name string + * + * @param encnam RFC NetBIOS encoded name + * @return Normal NetBIOS name string + */ + + public static String DecodeName(String encnam) + { + + // Check if the encoded name string is valid, must be 32 characters + + if (encnam == null || encnam.length() != 32) + return ""; + + // Convert the RFC NetBIOS name string to a normal NetBIOS name string + + String convstr = new String("ABCDEFGHIJKLMNOP"); + StringBuffer nameBuf = new StringBuffer(16); + + int idx = 0; + char ch1, ch2; + + while (idx < 32) + { + + // Get the current encoded character pair from the encoded name string + + ch1 = encnam.charAt(idx); + ch2 = encnam.charAt(idx + 1); + + if (ch1 == 'C' && ch2 == 'A') + { + + // Append a character + + nameBuf.append(' '); + } + else + { + + // Convert back to a character code + + int val = convstr.indexOf(ch1) << 4; + val += convstr.indexOf(ch2); + + // Append the current character to the decoded name + + nameBuf.append((char) (val & 0xFF)); + } + + // Update the encoded string index + + idx += 2; + + } // end while + + // Return the decoded string + + return nameBuf.toString(); + } + + /** + * Convert a host name string into RFC NetBIOS format. + * + * @param hostName Host name to be converted. + * @param nameType NetBIOS name type, added as the 16th byte of the name before conversion. + * @param buf Buffer to write the encoded name into. + * @param off Offset within the buffer to start writing. + * @return Buffer position + */ + public static int EncodeName(String hostName, char nameType, byte[] buf, int off) + { + + // Build the name string with the name type, make sure that the host + // name is uppercase. + + StringBuffer hName = new StringBuffer(hostName.toUpperCase()); + + if (hName.length() > 15) + hName.setLength(15); + + // Space pad the name then add the NetBIOS name type + + while (hName.length() < 15) + hName.append(' '); + hName.append(nameType); + + // Convert the NetBIOS name string to the RFC NetBIOS name format + + String convstr = new String("ABCDEFGHIJKLMNOP"); + int idx = 0; + int bufpos = off; + + // Set the name length byte + + buf[bufpos++] = 0x20; + + // Copy the encoded NetBIOS name to the buffer + + while (idx < hName.length()) + { + + // Get the current character from the host name string + + char ch = hName.charAt(idx++); + + if (ch == ' ') + { + + // Append an encoded character + + buf[bufpos++] = (byte) 'C'; + buf[bufpos++] = (byte) 'A'; + } + else + { + + // Append octet for the current character + + buf[bufpos++] = (byte) convstr.charAt((int) ch / 16); + buf[bufpos++] = (byte) convstr.charAt((int) ch % 16); + } + + } // end while + + // Null terminate the string + + buf[bufpos++] = 0; + return bufpos; + } + + /** + * Find a NetBIOS name on the network + * + * @param nbname NetBIOS name to search for, not yet RFC encoded + * @param nbType Name type, appended as the 16th byte of the name + * @param tmo Timeout value for receiving incoming datagrams + * @return NetBIOS name details + * @exception java.io.IOException If an I/O error occurs + */ + public static NetBIOSName FindName(String nbName, char nbType, int tmo) throws java.io.IOException + { + + // Call the main FindName method + + return FindName(new NetBIOSName(nbName, nbType, false), tmo); + } + + /** + * Find a NetBIOS name on the network + * + * @param nbname NetBIOS name to search for + * @param tmo Timeout value for receiving incoming datagrams + * @return NetBIOS name details + * @exception java.io.IOException If an I/O error occurs + */ + public static NetBIOSName FindName(NetBIOSName nbName, int tmo) throws java.io.IOException + { + + // Get the local address details + + InetAddress locAddr = InetAddress.getLocalHost(); + + // Create a datagram socket + + if (m_dgramSock == null) + { + + // Create a datagram socket + + m_dgramSock = new DatagramSocket(); + } + + // Set the datagram socket timeout, in milliseconds + + m_dgramSock.setSoTimeout(tmo); + + // Create a name lookup NetBIOS packet + + NetBIOSPacket nbpkt = new NetBIOSPacket(); + nbpkt.buildNameQueryRequest(nbName, m_tranIdx++); + + // Get the local host numeric address + + String locIP = locAddr.getHostAddress(); + int dotIdx = locIP.indexOf('.'); + if (dotIdx == -1) + return null; + + // If a WINS server has been configured the request is sent directly to the WINS server, if + // not then a broadcast is done on the local subnet. + + InetAddress destAddr = null; + + if (hasWINSServer() == false) + { + + // Check if the subnet mask has been set, if not then generate a subnet mask + + if (getSubnetMask() == null) + GenerateSubnetMask(null); + + // Build a broadcast destination address + + destAddr = InetAddress.getByName(getSubnetMask()); + } + else + { + + // Use the WINS server address + + destAddr = getWINSServer(); + } + + // Build the name lookup request + + DatagramPacket dgram = new DatagramPacket(nbpkt.getBuffer(), nbpkt.getLength(), destAddr, + RFCNetBIOSProtocol.NAME_PORT); + + // Allocate a receive datagram packet + + byte[] rxbuf = new byte[FindNameBufferSize]; + DatagramPacket rxdgram = new DatagramPacket(rxbuf, rxbuf.length); + + // Create a NetBIOS packet using the receive buffer + + NetBIOSPacket rxpkt = new NetBIOSPacket(rxbuf); + + // DEBUG + + if (m_debug) + nbpkt.DumpPacket(false); + + // Send the find name datagram + + m_dgramSock.send(dgram); + + // Receive a reply datagram + + boolean rxOK = false; + + do + { + + // Receive a datagram packet + + m_dgramSock.receive(rxdgram); + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + { + logger.debug("NetBIOS: Rx Datagram"); + rxpkt.DumpPacket(false); + } + + // Check if this is a valid response datagram + + if (rxpkt.isResponse() && rxpkt.getOpcode() == NetBIOSPacket.RESP_QUERY) + rxOK = true; + + } while (!rxOK); + + // Get the list of names from the response, should only be one name + + NetBIOSNameList nameList = rxpkt.getAnswerNameList(); + if (nameList != null && nameList.numberOfNames() > 0) + return nameList.getName(0); + return null; + } + + /** + * Build a list of nodes that own the specified NetBIOS name. + * + * @param nbname NetBIOS name to search for, not yet RFC encoded + * @param nbType Name type, appended as the 16th byte of the name + * @param tmo Timeout value for receiving incoming datagrams + * @return List of node name Strings + * @exception java.io.IOException If an I/O error occurs + */ + public static StringList FindNameList(String nbName, char nbType, int tmo) throws IOException + { + + // Get the local address details + + InetAddress locAddr = InetAddress.getLocalHost(); + + // Create a datagram socket + + if (m_dgramSock == null) + { + + // Create a datagram socket + + m_dgramSock = new DatagramSocket(); + } + + // Set the datagram socket timeout, in milliseconds + + m_dgramSock.setSoTimeout(tmo); + + // Create a name lookup NetBIOS packet + + NetBIOSPacket nbpkt = new NetBIOSPacket(); + + nbpkt.setTransactionId(m_tranIdx++); + nbpkt.setOpcode(NetBIOSPacket.NAME_QUERY); + nbpkt.setFlags(NetBIOSPacket.FLG_BROADCAST); + nbpkt.setQuestionCount(1); + nbpkt.setQuestionName(nbName, nbType, NetBIOSPacket.NAME_TYPE_NB, NetBIOSPacket.NAME_CLASS_IN); + + // Get the local host numeric address + + String locIP = locAddr.getHostAddress(); + int dotIdx = locIP.indexOf('.'); + if (dotIdx == -1) + return null; + + // If a WINS server has been configured the request is sent directly to the WINS server, if + // not then a broadcast is done on the local subnet. + + InetAddress destAddr = null; + + if (hasWINSServer() == false) + { + + // Check if the subnet mask has been set, if not then generate a subnet mask + + if (getSubnetMask() == null) + GenerateSubnetMask(null); + + // Build a broadcast destination address + + destAddr = InetAddress.getByName(getSubnetMask()); + } + else + { + + // Use the WINS server address + + destAddr = getWINSServer(); + } + + // Build the request datagram + + DatagramPacket dgram = new DatagramPacket(nbpkt.getBuffer(), nbpkt.getLength(), destAddr, + RFCNetBIOSProtocol.NAME_PORT); + + // Allocate a receive datagram packet + + byte[] rxbuf = new byte[FindNameBufferSize]; + DatagramPacket rxdgram = new DatagramPacket(rxbuf, rxbuf.length); + + // Create a NetBIOS packet using the receive buffer + + NetBIOSPacket rxpkt = new NetBIOSPacket(rxbuf); + + // DEBUG + + if (m_debug) + nbpkt.DumpPacket(false); + + // Create a vector to store the remote host addresses + + Vector addrList = new Vector(); + + // Calculate the end time, to stop receiving datagrams + + long endTime = System.currentTimeMillis() + tmo; + + // Send the find name datagram + + m_dgramSock.send(dgram); + + // Receive reply datagrams + + do + { + + // Receive a datagram packet + + try + { + m_dgramSock.receive(rxdgram); + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + { + logger.debug("NetBIOS: Rx Datagram"); + rxpkt.DumpPacket(false); + } + + // Check if this is a valid response datagram + + if (rxpkt.isResponse() && rxpkt.getOpcode() == NetBIOSPacket.RESP_QUERY) + { + + // Get the address of the remote host for this datagram and add it to the list + // of responders + + addrList.add(rxdgram.getAddress()); + } + } + catch (java.io.IOException ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + logger.debug(ex.toString()); + } + + } while (System.currentTimeMillis() < endTime); + + // Check if we received any replies + + if (addrList.size() == 0) + return null; + + // Create a node name list + + StringList nameList = new StringList(); + + // Convert the reply addresses to node names + + for (int i = 0; i < addrList.size(); i++) + { + + // Get the current address from the list + + InetAddress addr = addrList.elementAt(i); + + // Convert the address to a node name string + + String name = NetBIOSName(addr.getHostName()); + + // Check if the name is already in the name list + + if (!nameList.containsString(name)) + nameList.addString(name); + } + + // Return the node name list + + return nameList; + } + + /** + * Get the NetBIOS name list for the specified IP address + * + * @param ipAddr String + * @return NetBIOSNameList + */ + public static NetBIOSNameList FindNamesForAddress(String ipAddr) throws UnknownHostException, SocketException + { + + // Create a datagram socket + + if (m_dgramSock == null) + { + + // Create a datagram socket + + m_dgramSock = new DatagramSocket(); + } + + // Set the datagram socket timeout, in milliseconds + + m_dgramSock.setSoTimeout(2000); + + // Create a name lookup NetBIOS packet + + NetBIOSPacket nbpkt = new NetBIOSPacket(); + + nbpkt.setTransactionId(m_tranIdx++); + nbpkt.setOpcode(NetBIOSPacket.NAME_QUERY); + nbpkt.setFlags(NetBIOSPacket.FLG_BROADCAST); + nbpkt.setQuestionCount(1); + nbpkt.setQuestionName("*\0\0\0\0\0\0\0\0\0\0\0\0\0\0", NetBIOSName.WorkStation, NetBIOSPacket.NAME_TYPE_NBSTAT, + NetBIOSPacket.NAME_CLASS_IN); + + // Send the request to the specified address + + InetAddress destAddr = InetAddress.getByName(ipAddr); + DatagramPacket dgram = new DatagramPacket(nbpkt.getBuffer(), nbpkt.getLength(), destAddr, + RFCNetBIOSProtocol.NAME_PORT); + + // Allocate a receive datagram packet + + byte[] rxbuf = new byte[FindNameBufferSize]; + DatagramPacket rxdgram = new DatagramPacket(rxbuf, rxbuf.length); + + // Create a NetBIOS packet using the receive buffer + + NetBIOSPacket rxpkt = new NetBIOSPacket(rxbuf); + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + nbpkt.DumpPacket(false); + + // Create a vector to store the remote hosts NetBIOS names + + NetBIOSNameList nameList = null; + + try + { + + // Send the name query datagram + + m_dgramSock.send(dgram); + + // Receive a datagram packet + + m_dgramSock.receive(rxdgram); + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + { + logger.debug("NetBIOS: Rx Datagram"); + rxpkt.DumpPacket(false); + } + + // Check if this is a valid response datagram + + if (rxpkt.isResponse() && rxpkt.getOpcode() == NetBIOSPacket.RESP_QUERY && rxpkt.getAnswerCount() >= 1) + { + + // Get the received name list + + nameList = rxpkt.getAdapterStatusNameList(); + + // If the name list is valid update the names with the original address that was connected to + + if( nameList != null) + { + for ( int i = 0; i < nameList.numberOfNames(); i++) + { + NetBIOSName nbName = nameList.getName(i); + nbName.addIPAddress(destAddr.getAddress()); + } + } + } + } + catch (java.io.IOException ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + logger.debug(ex.toString()); + + // Unknown host + + throw new UnknownHostException(ipAddr); + } + + // Return the NetBIOS name list + + return nameList; + } + + /** + * Determine the subnet mask from the local hosts TCP/IP address + * + * @param addr TCP/IP address to set the subnet mask for, in 'nnn.nnn.nnn.nnn' format. + */ + public static String GenerateSubnetMask(String addr) throws java.net.UnknownHostException + { + + // Set the TCP/IP address string + + String localIP = addr; + + // Get the local TCP/IP address, if a null string has been specified + + if (localIP == null) + localIP = InetAddress.getLocalHost().getHostAddress(); + + // Find the location of the first dot in the TCP/IP address + + int dotPos = localIP.indexOf('.'); + if (dotPos != -1) + { + + // Extract the leading IP address value + + String ipStr = localIP.substring(0, dotPos); + int ipVal = Integer.valueOf(ipStr).intValue(); + + // Determine the subnet mask to use + + if (ipVal <= 127) + { + + // Class A address + + m_subnetMask = "" + ipVal + ".255.255.255"; + } + else if (ipVal <= 191) + { + + // Class B adddress + + dotPos++; + while (localIP.charAt(dotPos) != '.' && dotPos < localIP.length()) + dotPos++; + + if (dotPos < localIP.length()) + m_subnetMask = localIP.substring(0, dotPos) + ".255.255"; + } + else if (ipVal <= 223) + { + + // Class C address + + dotPos++; + int dotCnt = 1; + + while (dotCnt < 3 && dotPos < localIP.length()) + { + + // Check if the current character is a dot + + if (localIP.charAt(dotPos++) == '.') + dotCnt++; + } + + if (dotPos < localIP.length()) + m_subnetMask = localIP.substring(0, dotPos - 1) + ".255"; + } + } + + // Check if the subnet mask has been set, if not then use a general + // broadcast mask + + if (m_subnetMask == null) + { + + // Invalid TCP/IP address string format, use a general broadcast mask + // for now. + + m_subnetMask = "255.255.255.255"; + } + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Set subnet mask to " + m_subnetMask); + + // Return the subnet mask string + + return m_subnetMask; + } + + /** + * Get the WINS/NetBIOS name lookup timeout, in milliseconds. + * + * @return int + */ + public static int getLookupTimeout() + { + return m_lookupTmo; + } + + /** + * Return the name lookup type that is used when setting up new sessions, valid values are + * DNSOnly, WINSOnly, WINSAndDNS. DNSOnly is the default type. + * + * @return int + */ + public static int getLookupType() + { + return m_lookupType; + } + + /** + * Return the subnet mask string + * + * @return Subnet mask string, in 'nnn.nnn.nnn.nnn' format + */ + public static String getSubnetMask() + { + return m_subnetMask; + } + + /** + * Determine if the WINS server address is configured + * + * @return boolean + */ + public final static boolean hasWINSServer() + { + return m_winsServer != null ? true : false; + } + + /** + * Return the WINS server address + * + * @return InetAddress + */ + public final static InetAddress getWINSServer() + { + return m_winsServer; + } + + /** + * Determine if SMB session debugging is enabled + * + * @return true if debugging is enabled, else false. + */ + public static boolean isDebug() + { + return m_debug; + } + + /** + * Return the next session index + * + * @return int + */ + private final static synchronized int getSessionId() + { + return m_sessIdx++; + } + + /** + * Return the JVM unique id, used when generating caller names + * + * @return int + */ + public final static int getJVMIndex() + { + return m_jvmIdx; + } + + /** + * Convert the TCP/IP host name to a NetBIOS name string. + * + * @return java.lang.String + * @param hostName java.lang.String + */ + public static String NetBIOSName(String hostName) + { + + // Check if the host name contains a domain name + + String nbName = new String(hostName.toUpperCase()); + int pos = nbName.indexOf("."); + + if (pos != -1) + { + + // Strip the domain name for the NetBIOS name + + nbName = nbName.substring(0, pos); + } + + // Return the NetBIOS name string + + return nbName; + } + + /** + * Enable/disable NetBIOS session debugging + * + * @param dbg true to enable debugging, else false + */ + public static void setDebug(boolean dbg) + { + m_debug = dbg; + } + + /** + * Set the WINS/NetBIOS name lookup timeout value, in milliseconds. + * + * @param tmo int + */ + public static void setLookupTimeout(int tmo) + { + if (tmo >= 250) + m_lookupTmo = tmo; + } + + /** + * Set the name lookup type(s) to be used when opening new sessions, valid values are DNSOnly, + * WINSOnly, WINSAndDNS. DNSOnly is the default type. + * + * @param typ int + */ + public static void setLookupType(int typ) + { + if (typ >= DNSOnly && typ <= WINSAndDNS) + m_lookupType = typ; + } + + /** + * Set the subnet mask string + * + * @param subnet Subnet mask string, in 'nnn.nnn.nnn.nnn' format + */ + public static void setSubnetMask(String subnet) + { + m_subnetMask = subnet; + } + + /** + * Set the WINS server address + * + * @param addr InetAddress + */ + public final static void setWINSServer(InetAddress addr) + { + m_winsServer = addr; + } + + /** + * Get the NetBIOS adapter status for the specified node. + * + * @return java.util.Vector + * @param nodeName java.lang.String + */ + private static Vector AdapterStatus(String nodeName) throws java.io.IOException + { + + // Create the socket + + DatagramSocket nameSock = new DatagramSocket(); + + // Enable the timeout on the socket + + nameSock.setSoTimeout(2000); + + // Create an adapter status NetBIOS packet + + NetBIOSPacket nbpkt = new NetBIOSPacket(); + + // nbpkt.setTransactionId( m_tranIdx++); + nbpkt.setTransactionId(9999); + nbpkt.setOpcode(NetBIOSPacket.NAME_QUERY); + nbpkt.setFlags(NetBIOSPacket.FLG_BROADCAST); + nbpkt.setQuestionCount(1); + nbpkt.setQuestionName(nodeName, NetBIOSName.WorkStation, NetBIOSPacket.NAME_TYPE_NBSTAT, + NetBIOSPacket.NAME_CLASS_IN); + + // Build a broadcast destination address + + InetAddress destAddr = InetAddress.getByName(nodeName); + DatagramPacket dgram = new DatagramPacket(nbpkt.getBuffer(), nbpkt.getLength(), destAddr, + RFCNetBIOSProtocol.NAME_PORT); + + // Allocate a receive datagram packet + + byte[] rxbuf = new byte[512]; + DatagramPacket rxdgram = new DatagramPacket(rxbuf, rxbuf.length); + + // Create a NetBIOS packet using the receive buffer + + NetBIOSPacket rxpkt = new NetBIOSPacket(rxbuf); + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + nbpkt.DumpPacket(false); + + // Send the find name datagram + + nameSock.send(dgram); + + // Receive a reply datagram + + boolean rxOK = false; + + do + { + + // Receive a datagram packet + + nameSock.receive(rxdgram); + + // DEBUG + + if (logger.isDebugEnabled() && m_debug) + { + logger.debug("NetBIOS: Rx Datagram"); + rxpkt.DumpPacket(false); + } + + // Check if this is a valid response datagram + + if (rxpkt.isResponse() && rxpkt.getOpcode() == NetBIOSPacket.RESP_QUERY) + rxOK = true; + + } while (!rxOK); + + // Return the remote host address + + return null; + } + + /** + * Connect to a remote host. + * + * @param remHost Remote host node name/NetBIOS name. + * @param locName Local name/NetBIOS name. + * @param remAddr Optional remote address, if null then lookup will be done to convert name to + * address + * @exception java.io.IOException I/O error occurred. + * @exception java.net.UnknownHostException Remote host is unknown. + */ + public void Open(String remHost, String locName, String remAddr) throws java.io.IOException, + java.net.UnknownHostException + { + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Call " + remHost); + + // Convert the remote host name to an address + + boolean dnsLookup = false; + InetAddress addr = null; + + // Set the remote address is specified + + if (remAddr != null) + { + + // Use the specified remote address + + addr = InetAddress.getByName(remAddr); + } + else + { + + // Try a WINS/NetBIOS type name lookup, if enabled + + if (getLookupType() != DNSOnly) + { + try + { + NetBIOSName netName = FindName(remHost, NetBIOSName.FileServer, 500); + if (netName != null && netName.numberOfAddresses() > 0) + addr = InetAddress.getByName(netName.getIPAddressString(0)); + } + catch (Exception ex) + { + } + } + + // Try a DNS type name lookup, if enabled + + if (addr == null && getLookupType() != WINSOnly) + { + addr = InetAddress.getByName(remHost); + dnsLookup = true; + } + } + + // Check if we translated the remote host name to an address + + if (addr == null) + throw new java.net.UnknownHostException(remHost); + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Remote node hase address " + addr.getHostAddress() + " (" + + (dnsLookup ? "DNS" : "WINS") + ")"); + + // Determine the remote name to call + + String remoteName = null; + + if (getRemoteNameType() == NetBIOSName.FileServer && useWildcardFileServerName() == true) + remoteName = "*SMBSERVER"; + else + remoteName = remHost; + + // Open a session to the remote server + + int resp = openSession(remoteName, addr); + + // Check the server response + + if (resp == RFCNetBIOSProtocol.SESSION_ACK) + return; + else if (resp == RFCNetBIOSProtocol.SESSION_REJECT) + { + + // Try the connection again with the remote host name + + if (remoteName.equals(remHost) == false) + resp = openSession(remHost, addr); + + // Check if we got a valid response this time + + if (resp == RFCNetBIOSProtocol.SESSION_ACK) + return; + + // Server rejected the connection + + throw new java.io.IOException("NetBIOS session reject"); + } + else if (resp == RFCNetBIOSProtocol.SESSION_RETARGET) + throw new java.io.IOException("NetBIOS ReTarget"); + + // Invalid session response, hangup the session + + Close(); + throw new java.io.IOException("Invalid NetBIOS response, 0x" + Integer.toHexString(resp)); + } + + /** + * Open a NetBIOS session to a remote server + * + * @param remoteName String + * @param addr InetAddress + * @return int + * @exception IOException + */ + private final int openSession(String remoteName, InetAddress addr) throws IOException + { + + // Create the socket + + m_nbSocket = new Socket(addr, m_remotePort); + + // Enable the timeout on the socket, and disable Nagle algorithm + + m_nbSocket.setSoTimeout(m_tmo); + m_nbSocket.setTcpNoDelay(true); + + // Attach input/output streams to the socket + + m_nbIn = new DataInputStream(m_nbSocket.getInputStream()); + m_nbOut = new DataOutputStream(m_nbSocket.getOutputStream()); + + // Allocate a buffer to receive the session response + + byte[] inpkt = new byte[RFCNetBIOSProtocol.SESSRESP_LEN]; + + // Create the from/to NetBIOS names + + NetBIOSName fromName = createUniqueCallerName(); + NetBIOSName toName = new NetBIOSName(remoteName, getRemoteNameType(), false); + + // Debug + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Call from " + fromName + " to " + toName); + + // Build the session request packet + + NetBIOSPacket nbPkt = new NetBIOSPacket(); + nbPkt.buildSessionSetupRequest(fromName, toName); + + // Send the session request packet + + m_nbOut.write(nbPkt.getBuffer(), 0, nbPkt.getLength()); + + // Allocate a buffer for the session request response, and read the response + + int resp = -1; + + if (m_nbIn.read(inpkt, 0, RFCNetBIOSProtocol.SESSRESP_LEN) >= RFCNetBIOSProtocol.HEADER_LEN) + { + + // Check the session request response + + resp = (int) (inpkt[0] & 0xFF); + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Rx " + NetBIOSPacket.getTypeAsString(resp)); + } + + // Check for a positive response + + if (resp != RFCNetBIOSProtocol.SESSION_ACK) + { + + // Close the socket and streams + + m_nbIn.close(); + m_nbIn = null; + + m_nbOut.close(); + m_nbOut = null; + + m_nbSocket.close(); + m_nbSocket = null; + } + + // Return the response code + + return resp; + } + + /** + * Return the local NetBIOS name type. + * + * @return char + */ + public char getLocalNameType() + { + return m_locNameType; + } + + /** + * Return the remote NetBIOS name type. + * + * @return char + */ + public char getRemoteNameType() + { + return m_remNameType; + } + + /** + * Get the session timeout value + * + * @return NetBIOS session timeout value + */ + public int getTimeout() + { + return m_tmo; + } + + /** + * Close the NetBIOS session. + * + * @exception IOException If an I/O error occurs + */ + public void Close() throws IOException + { + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: HangUp"); + + // Close the session if active + + if (m_nbSocket != null) + { + m_nbSocket.close(); + m_nbSocket = null; + } + } + + /** + * Receive a data packet from the remote host. + * + * @param buf Byte buffer to receive the data into. + * @param tmo Receive timeout in milliseconds, or zero for no timeout + * @return Length of the received data. + * @exception java.io.IOException I/O error occurred. + */ + public int Receive(byte[] buf, int tmo) throws java.io.IOException + { + + // Set the read timeout + + if (tmo != m_tmo) + { + m_nbSocket.setSoTimeout(tmo); + m_tmo = tmo; + } + + // Read a data packet, dump any session keep alive packets + + int pkttyp; + int rdlen; + + do + { + + // Read a packet header + + rdlen = m_nbIn.read(buf, 0, RFCNetBIOSProtocol.HEADER_LEN); + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Read " + rdlen + " bytes"); + + // Check if a header was received + + if (rdlen < RFCNetBIOSProtocol.HEADER_LEN) + throw new java.io.IOException("NetBIOS Short Read"); + + // Get the packet type from the header + + pkttyp = (int) (buf[0] & 0xFF); + + } while (pkttyp == RFCNetBIOSProtocol.SESSION_KEEPALIVE); + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Rx Pkt Type = " + pkttyp + ", " + Integer.toHexString(pkttyp)); + + // Check that the packet is a session data packet + + if (pkttyp != RFCNetBIOSProtocol.SESSION_MESSAGE) + throw new java.io.IOException("NetBIOS Unknown Packet Type, " + pkttyp); + + // Extract the data size from the packet header + + int pktlen = (int) DataPacker.getShort(buf, 2); + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Rx Data Len = " + pktlen); + + // Check if the user buffer is long enough to contain the data + + if (buf.length < (pktlen + RFCNetBIOSProtocol.HEADER_LEN)) + { + + // Debug mode + + logger.debug("NetBIOS: Rx Pkt Type = " + pkttyp + ", " + Integer.toHexString(pkttyp)); + logger.debug("NetBIOS: Rx Buf Too Small pkt=" + pktlen + " buflen=" + buf.length); + HexDump.Dump(buf, 16, 0); + + throw new java.io.IOException("NetBIOS Recv Buffer Too Small (pkt=" + pktlen + "/buf=" + buf.length + ")"); + } + + // Read the data part of the packet into the users buffer, this may take + // several reads + + int totlen = 0; + int offset = RFCNetBIOSProtocol.HEADER_LEN; + + while (pktlen > 0) + { + + // Read the data + + rdlen = m_nbIn.read(buf, offset, pktlen); + + // Update the received length and remaining data length + + totlen += rdlen; + pktlen -= rdlen; + + // Update the user buffer offset as more reads will be required + // to complete the data read + + offset += rdlen; + + } // end while reading data + + // Return the received data length, not including the NetBIOS header + + return totlen; + } + + /** + * Send a data packet to the remote host. + * + * @param data Byte array containing the data to be sent. + * @param siz Length of the data to send. + * @return true if the data was sent successfully, else false. + * @exception java.io.IOException I/O error occurred. + */ + public boolean Send(byte[] data, int siz) throws java.io.IOException + { + + // Check that the session is valid + + if (m_nbSocket == null) + return false; + + // Debug mode + + if (logger.isDebugEnabled() && m_debug) + logger.debug("NetBIOS: Tx " + siz + " bytes"); + + // Fill in the NetBIOS message header, this is already allocated as + // part of the users buffer. + + data[0] = (byte) RFCNetBIOSProtocol.SESSION_MESSAGE; + data[1] = (byte) 0; + + DataPacker.putShort((short) siz, data, 2); + + // Output the data packet + + int bufSiz = siz + RFCNetBIOSProtocol.HEADER_LEN; + m_nbOut.write(data, 0, bufSiz); + return true; + } + + /** + * Set the local NetBIOS name type for this session. + * + * @param nameType int + */ + public void setLocalNameType(char nameType) + { + m_locNameType = nameType; + } + + /** + * Set the remote NetBIOS name type. + * + * @param param char + */ + public void setRemoteNameType(char nameType) + { + m_remNameType = nameType; + } + + /** + * Set the session timeout value + * + * @param tmo Session timeout value + */ + public void setTimeout(int tmo) + { + m_tmo = tmo; + } + + /** + * Set the caller session name template string that is appended to the local host name to create + * a unique caller name. + * + * @param template String + * @exception NameTemplateExcepition + */ + public final static void setCallerNameTemplate(String template) throws NameTemplateException + { + + // Check if the template string is valid, is not too long + + if (template == null || template.length() == 0 || template.length() > MaxCallerNameTemplateLength) + throw new NameTemplateException("Invalid template string, " + template); + + // Template must contain at least one session id template character + + if (template.indexOf(SessionIdChar) == -1) + throw new NameTemplateException("No session id character in template"); + + // Check if the template contains any invalid characters + + for (int i = 0; i < template.length(); i++) + { + if (ValidTemplateChars.indexOf(template.charAt(i)) == -1) + throw new NameTemplateException("Invalid character in template, '" + template.charAt(i) + "'"); + } + + // Set the caller name template string + + m_callerTemplate = template; + + // Clear the local name part string so that it will be regenerated to match the new template + // string + + m_localNamePart = null; + } + + /** + * Set the JVM index, used to generate unique caller names when multiple JVMs are run on the + * same host. + * + * @param jvmIdx int + */ + public final static void setJVMIndex(int jvmIdx) + { + if (jvmIdx >= 0) + m_jvmIdx = jvmIdx; + } + + /** + * Create a unique caller name for a new NetBIOS session. The unique name contains the local + * host name plus an index that is unique for this JVM, plus an optional JVM index. + * + * @return NetBIOSName + */ + private final NetBIOSName createUniqueCallerName() + { + + // Check if the local name part has been set + + if (m_localNamePart == null) + { + + String localName = null; + + try + { + localName = InetAddress.getLocalHost().getHostName(); + } + catch (Exception ex) + { + } + + // Check if the name contains a domain + + int pos = localName.indexOf("."); + + if (pos != -1) + localName = localName.substring(0, pos); + + // Truncate the name if the host name plus the template is longer than 15 characters. + + int nameLen = 16 - m_callerTemplate.length(); + + if (localName.length() > nameLen) + localName = localName.substring(0, nameLen - 1); + + // Set the local host name part + + m_localNamePart = localName.toUpperCase(); + } + + // Get a unique session id and the unique JVM id + + int sessId = getSessionId(); + int jvmId = getJVMIndex(); + + // Build the NetBIOS name string + + StringBuffer nameBuf = new StringBuffer(16); + + nameBuf.append(m_localNamePart); + + // Process the caller name template string + + int idx = 0; + int len = -1; + + while (idx < m_callerTemplate.length()) + { + + // Get the current template character + + char ch = m_callerTemplate.charAt(idx++); + + switch (ch) + { + + // Session id + + case SessionIdChar: + len = findRepeatLength(m_callerTemplate, idx, SessionIdChar); + appendZeroPaddedHexValue(sessId, len, nameBuf); + idx += len - 1; + break; + + // JVM id + + case JVMIdChar: + len = findRepeatLength(m_callerTemplate, idx, JVMIdChar); + appendZeroPaddedHexValue(jvmId, len, nameBuf); + idx += len - 1; + break; + + // Pass any other characters through to the name string + + default: + nameBuf.append(ch); + break; + } + } + + // Create the NetBIOS name object + + return new NetBIOSName(nameBuf.toString(), getLocalNameType(), false); + } + + /** + * Find the length of the character block in the specified string + * + * @param str String + * @param pos int + * @param ch char + * @return int + */ + private final int findRepeatLength(String str, int pos, char ch) + { + int len = 1; + + while (pos < str.length() && str.charAt(pos++) == ch) + len++; + return len; + } + + /** + * Append a zero filled hex string to the specified string + * + * @param val int + * @param len int + * @param str StringBuffer + */ + private final void appendZeroPaddedHexValue(int val, int len, StringBuffer str) + { + + // Create the hex string of the value + + String hex = Integer.toHexString(val); + + // Pad the final string as required + + for (int i = 0; i < len - hex.length(); i++) + str.append("0"); + str.append(hex); + } + + /** + * Return the default socket timeout value + * + * @return int + */ + public static final int getDefaultTimeout() + { + return _defTimeout; + } + + /** + * Set the default socket timeout for new sessions + * + * @param tmo int + */ + public static final void setDefaultTimeout(int tmo) + { + _defTimeout = tmo; + } + + /** + * Return the use wildcard file server name flag status. If true the target name when conencting + * to a remote file server will be '*SMBSERVER', if false the remote name will be used. + * + * @return boolean + */ + public static final boolean useWildcardFileServerName() + { + return m_useWildcardFileServer; + } + + /** + * Set the use wildcard file server name flag. If true the target name when conencting to a + * remote file server will be '*SMBSERVER', if false the remote name will be used. + * + * @param useWildcard boolean + */ + public static final void setWildcardFileServerName(boolean useWildcard) + { + m_useWildcardFileServer = useWildcard; + } + + /** + * Finalize the NetBIOS session object + */ + protected void finalize() + { + + // Close the socket + + if (m_nbSocket != null) + { + try + { + m_nbSocket.close(); + } + catch (java.io.IOException ex) + { + } + m_nbSocket = null; + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/NetworkSettings.java b/source/java/org/alfresco/filesys/netbios/NetworkSettings.java new file mode 100644 index 0000000000..f73dfa4f4b --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/NetworkSettings.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +import java.net.InetAddress; + +/** + * The network settings class contains various Windows Networking settings that are needed by the + * NetBIOS and SMB layers. + */ +public class NetworkSettings +{ + + // Broadcast mask for broadcast messages + + private static String m_broadcastMask; + + // Domain name/workgroup that this node is part of + + private static String m_domain; + + // Subnet mask address + + private static InetAddress m_subnetAddr; + + /** + * Determine the boradcast mask from the local hosts TCP/IP address + * + * @param addr TCP/IP address to set the broadcast mask for, in 'nnn.nnn.nnn.nnn' format. + */ + public static String GenerateBroadcastMask(String addr) throws java.net.UnknownHostException + { + + // Check if the broadcast mask has already been set + + if (m_broadcastMask != null) + return m_broadcastMask; + + // Set the TCP/IP address string + + String localIP = addr; + + if (localIP == null) + localIP = InetAddress.getLocalHost().getHostAddress(); + + // Find the location of the first dot in the TCP/IP address + + int dotPos = localIP.indexOf('.'); + if (dotPos != -1) + { + + // Extract the leading IP address value + + String ipStr = localIP.substring(0, dotPos); + int ipVal = Integer.valueOf(ipStr).intValue(); + + // Determine the broadcast mask to use + + if (ipVal <= 127) + { + + // Class A address + + m_broadcastMask = "" + ipVal + ".255.255.255"; + } + else if (ipVal <= 191) + { + + // Class B adddress + + dotPos++; + while (localIP.charAt(dotPos) != '.' && dotPos < localIP.length()) + dotPos++; + + if (dotPos < localIP.length()) + m_broadcastMask = localIP.substring(0, dotPos) + ".255.255"; + } + else if (ipVal <= 223) + { + + // Class C address + + dotPos++; + int dotCnt = 1; + + while (dotCnt < 3 && dotPos < localIP.length()) + { + + // Check if the current character is a dot + + if (localIP.charAt(dotPos++) == '.') + dotCnt++; + } + + if (dotPos < localIP.length()) + m_broadcastMask = localIP.substring(0, dotPos - 1) + ".255"; + } + } + + // Check if the broadcast mask has been set, if not then use a general + // broadcast mask + + if (m_broadcastMask == null) + { + + // Invalid TCP/IP address string format, use a general broadcast mask + // for now. + + m_broadcastMask = "255.255.255.255"; + } + + // Return the broadcast mask string + + return m_broadcastMask; + } + + /** + * Return the broadcast mask as an address. + * + * @return java.net.InetAddress + */ + public final static InetAddress getBroadcastAddress() throws java.net.UnknownHostException + { + + // Check if the subnet address is valid + + if (m_subnetAddr == null) + { + + // Generate the subnet mask + + String subnet = GenerateBroadcastMask(null); + m_subnetAddr = InetAddress.getByName(subnet); + } + + // Return the subnet mask address + + return m_subnetAddr; + } + + /** + * Get the broadcast mask. + * + * @return java.lang.String + */ + public static String getBroadcastMask() + { + return m_broadcastMask; + } + + /** + * Get the local domain/workgroup name. + */ + public static String getDomain() + { + return m_domain; + } + + /** + * Determine if the broadcast mask has been setup. + */ + public static boolean hasBroadcastMask() + { + if (m_broadcastMask == null) + return false; + return true; + } + + /** + * Set the broadcast mask to be used for broadcast packets. + * + * @param mask java.lang.String + */ + public static void setBroadcastMask(String mask) + { + m_broadcastMask = mask; + } + + /** + * Set the local domain/workgroup name. + * + * @param domain java.lang.String + */ + public static void setDomain(String domain) + { + m_domain = domain; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/RFCNetBIOSProtocol.java b/source/java/org/alfresco/filesys/netbios/RFCNetBIOSProtocol.java new file mode 100644 index 0000000000..b6b2a48512 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/RFCNetBIOSProtocol.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios; + +/** + * RFC NetBIOS constants. + */ +public final class RFCNetBIOSProtocol +{ + + // RFC NetBIOS default port/socket + + public static final int PORT = 139; + + // RFC NetBIOS datagram port + + public static final int DATAGRAM = 138; + + // RFC NetBIOS default name lookup datagram port + + public static final int NAME_PORT = 137; + + // RFC NetBIOS default socket timeout + + public static final int TMO = 30000; // 30 seconds, in milliseconds + + // RFC NetBIOS message types. + + public static final int SESSION_MESSAGE = 0x00; + public static final int SESSION_REQUEST = 0x81; + public static final int SESSION_ACK = 0x82; + public static final int SESSION_REJECT = 0x83; + public static final int SESSION_RETARGET = 0x84; + public static final int SESSION_KEEPALIVE = 0x85; + + // RFC NetBIOS packet header length, and various message lengths. + + public static final int HEADER_LEN = 4; + public static final int SESSREQ_LEN = 72; + public static final int SESSRESP_LEN = 9; + + // Maximum packet size that RFC NetBIOS can handle (17bit value) + + public static final int MaxPacketSize = 0x01FFFF + HEADER_LEN; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/server/AddNameListener.java b/source/java/org/alfresco/filesys/netbios/server/AddNameListener.java new file mode 100644 index 0000000000..34747ed168 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/AddNameListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +/** + * NetBIOS add name listener interface. + */ +public interface AddNameListener +{ + /** + * Signal that a NetBIOS name has been added, or an error occurred whilst trying to add a new + * NetBIOS name. + * + * @param evt NetBIOSNameEvent + */ + public void netbiosNameAdded(NetBIOSNameEvent evt); +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/server/NetBIOSNameEvent.java b/source/java/org/alfresco/filesys/netbios/server/NetBIOSNameEvent.java new file mode 100644 index 0000000000..31c7635075 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/NetBIOSNameEvent.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +import org.alfresco.filesys.netbios.NetBIOSName; + +/** + * NetBIOS name server event class. + */ +public class NetBIOSNameEvent +{ + /* + * NetBIOS name event status codes + */ + + public static final int ADD_SUCCESS = 0; // local name added successfully + public static final int ADD_FAILED = 1; // local name add failure + public static final int ADD_DUPLICATE = 2; // local name already in use + public static final int ADD_IOERROR = 3; // I/O error during add name broadcast + public static final int QUERY_NAME = 4; // query for local name + public static final int REGISTER_NAME = 5; // remote name registered + public static final int REFRESH_NAME = 6; // name refresh + public static final int REFRESH_IOERROR = 7; // refresh name I/O error + + /** + * NetBIOS name details + */ + + private NetBIOSName m_name; + + /** + * Name status + */ + + private int m_status; + + /** + * Create a NetBIOS name event. + * + * @param name NetBIOSName + * @param sts int + */ + protected NetBIOSNameEvent(NetBIOSName name, int sts) + { + m_name = name; + m_status = sts; + } + + /** + * Return the NetBIOS name details. + * + * @return NetBIOSName + */ + public final NetBIOSName getNetBIOSName() + { + return m_name; + } + + /** + * Return the NetBIOS name status. + * + * @return int + */ + public final int getStatus() + { + return m_status; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/server/NetBIOSNameServer.java b/source/java/org/alfresco/filesys/netbios/server/NetBIOSNameServer.java new file mode 100644 index 0000000000..2db340c4ca --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/NetBIOSNameServer.java @@ -0,0 +1,1933 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.NetBIOSPacket; +import org.alfresco.filesys.netbios.NetworkSettings; +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.NetworkServer; +import org.alfresco.filesys.server.ServerListener; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * NetBIOS name server class. + */ +public class NetBIOSNameServer extends NetworkServer implements Runnable +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol.netbios"); + + // Server version + + private static final String ServerVersion = "3.5.0"; + + // Various NetBIOS packet sizes + + public static final int AddNameSize = 256; + public static final int DeleteNameSize = 256; + public static final int RefreshNameSize = 256; + + // Add name thread broadcast interval and retry count + + private static final int AddNameInterval = 2000; // ms between transmits + private static final int AddNameRetries = 5; // number of broadcasts + + private static final int AddNameWINSInterval = 250; // ms between requests when using WINS + + // Delete name interval and retry count + + private static final int DeleteNameInterval = 200; // ms between transmits + private static final int DeleteNameRetries = 1; // number of broadcasts + + // Refresh name retry count + + public static final int RefreshNameRetries = 2; // number of broadcasts + + // NetBIOS flags + + public static final int GroupName = 0x8000; + + // Default time to live value for names registered by this server, in seconds + + public static final int DefaultTTL = 10800; // 3 hours + + // Name refresh thread wakeup interval + + public static final long NameRefreshWakeupInterval = 180000L; // 3 minutes + + // Name transaction id + + private static int m_tranId; + + // NetBIOS name service datagram socket + + private DatagramSocket m_socket; + + // Shutdown flag + + private boolean m_shutdown; + + // Local address to bind the name server to + + private InetAddress m_bindAddress; + + // Broadcast address, if not using WINS + + private InetAddress m_bcastAddr; + + // Port/socket to bind to + + private int m_port = RFCNetBIOSProtocol.NAME_PORT; + + // WINS server addresses + + private InetAddress m_winsPrimary; + private InetAddress m_winsSecondary; + + // Local add name listener list + + private Vector m_addListeners; + + // Local name query listener list + + private Vector m_queryListeners; + + // Remote name add listener list + + private Vector m_remoteListeners; + + // Local NetBIOS name table + + private Vector m_localNames; + + // Remote NetBIOS name table + + private Hashtable m_remoteNames; + + // List of active add name requests + + private Vector m_reqList; + + // NetBIOS request handler and name refresh threads + + private NetBIOSRequestHandler m_reqHandler; + private NetBIOSNameRefresh m_refreshThread; + + // Server thread + + private Thread m_srvThread; + + // NetBIOS request handler thread inner class + + class NetBIOSRequestHandler extends Thread + { + + // Shutdown request flag + + private boolean m_hshutdown = false; + + /** + * Default constructor + */ + public NetBIOSRequestHandler() + { + setDaemon(true); + setName("NetBIOSRequest"); + } + + /** + * Shutdown the request handler thread + */ + public final void shutdownRequest() + { + m_hshutdown = true; + + synchronized (m_reqList) + { + m_reqList.notify(); + } + } + + /** + * Main thread code + */ + public void run() + { + + // Loop until shutdown requested + + while (m_hshutdown == false) + { + + try + { + + // Wait for something to do + + NetBIOSRequest req = null; + + synchronized (m_reqList) + { + + // Check if there are any requests in the queue + + if (m_reqList.size() == 0) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("NetBIOS handler waiting for request ..."); + + // Wait for some work ... + + m_reqList.wait(); + } + + // Remove a request from the queue + + if (m_reqList.size() > 0) + req = m_reqList.get(0); + else if (m_hshutdown == true) + break; + } + + // Get the request retry count, for WINS only send one request + + int reqRetry = req.getRetryCount(); + if (hasPrimaryWINSServer()) + reqRetry = 1; + + // Process the request + + boolean txsts = true; + int retry = 0; + + while (req.hasErrorStatus() == false && retry++ < reqRetry) + { + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("NetBIOS handler, processing " + req); + + // Process the request + + switch (req.isType()) + { + + // Add name request + + case NetBIOSRequest.AddName: + + // Check if a WINS server is configured + + if (hasPrimaryWINSServer()) + txsts = sendAddName(req, getPrimaryWINSServer(), false); + else + txsts = sendAddName(req, getBroadcastAddress(), true); + break; + + // Delete name request + + case NetBIOSRequest.DeleteName: + + // Check if a WINS server is configured + + if (hasPrimaryWINSServer()) + txsts = sendDeleteName(req, getPrimaryWINSServer(), false); + else + txsts = sendDeleteName(req, getBroadcastAddress(), true); + break; + + // Refresh name request + + case NetBIOSRequest.RefreshName: + + // Check if a WINS server is configured + + if (hasPrimaryWINSServer()) + txsts = sendRefreshName(req, getPrimaryWINSServer(), false); + else + txsts = sendRefreshName(req, getBroadcastAddress(), true); + break; + } + + // Check if the request was successful + + if (txsts == true && req.getRetryInterval() > 0) + { + + // Sleep for a while + + sleep(req.getRetryInterval()); + } + } + + // Check if the request was successful + + if (req.hasErrorStatus() == false) + { + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("NetBIOS handler successful, " + req); + + // Update the name record + + NetBIOSName nbName = req.getNetBIOSName(); + + switch (req.isType()) + { + + // Add name request + + case NetBIOSRequest.AddName: + + // Add the name to the list of local names + + if (m_localNames.contains(nbName) == false) + m_localNames.addElement(nbName); + + // Update the expiry time for the name + + nbName.setExpiryTime(System.currentTimeMillis() + (nbName.getTimeToLive() * 1000L)); + + // Inform listeners that the request was successful + + fireAddNameEvent(nbName, NetBIOSNameEvent.ADD_SUCCESS); + break; + + // Delete name request + + case NetBIOSRequest.DeleteName: + + // Remove the name from the list of local names + + m_localNames.remove(req.getNetBIOSName()); + break; + + // Refresh name registration request + + case NetBIOSRequest.RefreshName: + + // Update the expiry time for the name + + nbName.setExpiryTime(System.currentTimeMillis() + (nbName.getTimeToLive() * 1000L)); + break; + } + } + else + { + + // Error occurred + + switch (req.isType()) + { + + // Add name request + + case NetBIOSRequest.AddName: + + // Remove the name from the local name list + + m_localNames.remove(req.getNetBIOSName()); + break; + } + } + + // Remove the request from the queue + + synchronized (m_reqList) + { + m_reqList.remove(0); + } + } + catch (InterruptedException ex) + { + } + + // Check if the request handler has been shutdown + + if (m_hshutdown == true) + break; + } + } + + /** + * Send an add name request + * + * @param req NetBIOSRequest + * @param dest InetAddress + * @param bcast boolean + * @return boolean + */ + private final boolean sendAddName(NetBIOSRequest req, InetAddress dest, boolean bcast) + { + + try + { + + // Allocate a buffer for the add name NetBIOS packet + + byte[] buf = new byte[AddNameSize]; + NetBIOSPacket addPkt = new NetBIOSPacket(buf); + + // Build an add name packet for each IP address + + for (int i = 0; i < req.getNetBIOSName().numberOfAddresses(); i++) + { + + // Build an add name request for the current IP address + + int len = addPkt.buildAddNameRequest(req.getNetBIOSName(), i, req.getTransactionId()); + if (bcast == false) + addPkt.setFlags(0); + + // Allocate the datagram packet, using the add name buffer + + DatagramPacket pkt = new DatagramPacket(buf, len, dest, getPort()); + + // Send the add name request + + if (m_socket != null) + m_socket.send(pkt); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug(" Add name " + (bcast ? "broadcast" : "WINS") + ", " + req); + } + } + catch (IOException ex) + { + fireAddNameEvent(req.getNetBIOSName(), NetBIOSNameEvent.ADD_IOERROR); + req.setErrorStatus(true); + return false; + } + + // Add name broadcast successful + + return true; + } + + /** + * Send a refresh name request + * + * @param req NetBIOSRequest + * @param dest InetAddress + * @param bcast boolean + * @return boolean + */ + private final boolean sendRefreshName(NetBIOSRequest req, InetAddress dest, boolean bcast) + { + + try + { + + // Allocate a buffer for the refresh name NetBIOS packet + + byte[] buf = new byte[RefreshNameSize]; + NetBIOSPacket refreshPkt = new NetBIOSPacket(buf); + + // Build a refresh name packet for each IP address + + for (int i = 0; i < req.getNetBIOSName().numberOfAddresses(); i++) + { + + // Build a refresh name request for the current IP address + + int len = refreshPkt.buildRefreshNameRequest(req.getNetBIOSName(), i, req.getTransactionId()); + if (bcast == false) + refreshPkt.setFlags(0); + + // Allocate the datagram packet, using the refresh name buffer + + DatagramPacket pkt = new DatagramPacket(buf, len, dest, getPort()); + + // Send the refresh name request + + if (m_socket != null) + m_socket.send(pkt); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug(" Refresh name " + (bcast ? "broadcast" : "WINS") + ", " + req); + } + } + catch (IOException ex) + { + req.setErrorStatus(true); + return false; + } + + // Add name broadcast successful + + return true; + } + + /** + * Send a delete name request via a network broadcast + * + * @param req NetBIOSRequest + * @param dest InetAddress + * @param bcast boolean + * @return boolean + */ + private final boolean sendDeleteName(NetBIOSRequest req, InetAddress dest, boolean bcast) + { + + try + { + + // Allocate a buffer for the delete name NetBIOS packet + + byte[] buf = new byte[DeleteNameSize]; + NetBIOSPacket delPkt = new NetBIOSPacket(buf); + + // Build a delete name packet for each IP address + + for (int i = 0; i < req.getNetBIOSName().numberOfAddresses(); i++) + { + + // Build an add name request for the current IP address + + int len = delPkt.buildDeleteNameRequest(req.getNetBIOSName(), i, req.getTransactionId()); + if (bcast == false) + delPkt.setFlags(0); + + // Allocate the datagram packet, using the add name buffer + + DatagramPacket pkt = new DatagramPacket(buf, len, dest, getPort()); + + // Send the add name request + + if (m_socket != null) + m_socket.send(pkt); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug(" Delete name " + (bcast ? "broadcast" : "WINS") + ", " + req); + } + } + catch (IOException ex) + { + req.setErrorStatus(true); + return false; + } + + // Delete name broadcast successful + + return true; + } + }; + + // NetBIOS name refresh thread inner class + + class NetBIOSNameRefresh extends Thread + { + + // Shutdown request flag + + private boolean m_hshutdown = false; + + /** + * Default constructor + */ + public NetBIOSNameRefresh() + { + setDaemon(true); + setName("NetBIOSRefresh"); + } + + /** + * Shutdown the name refresh thread + */ + public final void shutdownRequest() + { + m_hshutdown = true; + + // Wakeup the thread + + this.interrupt(); + } + + /** + * Main thread code + */ + public void run() + { + + // Loop for ever + + while (m_hshutdown == false) + { + + try + { + + // Sleep for a while + + sleep(NameRefreshWakeupInterval); + + // Check if there is a shutdown pending + + if (m_hshutdown == true) + break; + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("NetBIOS name refresh wakeup ..."); + + // Check if there are any registered names that will expire in the next interval + + synchronized (m_localNames) + { + + // Get the current time plus the wakeup interval + + long expireTime = System.currentTimeMillis() + NameRefreshWakeupInterval; + + // Loop through the local name list + + for (int i = 0; i < m_localNames.size(); i++) + { + + // Get a name from the list + + NetBIOSName nbName = m_localNames.get(i); + + // Check if the name has expired, or will expire before the next wakeup + // event + + if (nbName.getExpiryTime() < expireTime) + { + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("Queuing name refresh for " + nbName); + + // Queue a refresh request for the NetBIOS name + + NetBIOSRequest nbReq = new NetBIOSRequest(NetBIOSRequest.RefreshName, nbName, + getNextTransactionId()); + nbReq.setRetryCount(RefreshNameRetries); + + // Queue the request + + synchronized (m_reqList) + { + + // Add the request to the list + + m_reqList.addElement(nbReq); + + // Wakeup the processing thread + + m_reqList.notify(); + } + } + } + } + } + catch (Exception ex) + { + + // Debug + + if ( m_hshutdown == false) + logger.error("NetBIOS Name refresh thread exception", ex); + } + } + } + }; + + /** + * Default constructor + * + * @param serviceRegistry repository connection + * @param config ServerConfiguration + * @exception SocketException If a network setup error occurs + */ + public NetBIOSNameServer(ServerConfiguration config) throws SocketException + { + super("NetBIOS", config); + + // Perform common constructor code + commonConstructor(); + } + + /** + * Common constructor code + * + * @exception SocketException If a network setup error occurs + */ + private final void commonConstructor() throws SocketException + { + + // Set the server version + + setVersion(ServerVersion); + + // Allocate the local and remote name tables + + m_localNames = new Vector(); + m_remoteNames = new Hashtable(); + + // Check if NetBIOS name server debug output is enabled + + if (getConfiguration().hasNetBIOSDebug()) + setDebug(true); + + // Set the local address to bind the server to, and server port + + setBindAddress(getConfiguration().getNetBIOSBindAddress()); + setServerPort(RFCNetBIOSProtocol.NAME_PORT); + + // Copy the WINS server addresses, if set + + setPrimaryWINSServer(getConfiguration().getPrimaryWINSServer()); + setSecondaryWINSServer(getConfiguration().getSecondaryWINSServer()); + + // Check if WINS is not enabled, use broadcasts instead + + if (hasPrimaryWINSServer() == false) + { + + try + { + m_bcastAddr = InetAddress.getByName(getConfiguration().getBroadcastMask()); + } + catch (Exception ex) + { + } + } + } + + /** + * Return the local address the server binds to, or null if all local addresses are used. + * + * @return java.net.InetAddress + */ + public final InetAddress getBindAddress() + { + return m_bindAddress; + } + + /** + * Return the next available transaction id for outgoing NetBIOS packets. + * + * @return int + */ + protected final synchronized int getNextTransactionId() + { + return m_tranId++; + } + + /** + * Return the port/socket that the server is bound to. + * + * @return int + */ + public final int getPort() + { + return m_port; + } + + /** + * Determine if the server binds to a particulat local address, or all addresses + * + * @return boolean + */ + public final boolean hasBindAddress() + { + return m_bindAddress != null ? true : false; + } + + /** + * Return the remote name table + * + * @return Hashtable + */ + public final Hashtable getNameTable() + { + return m_remoteNames; + } + + /** + * Return the broadcast address, if WINS is disabled + * + * @return InetAddress + */ + public final InetAddress getBroadcastAddress() + { + return m_bcastAddr; + } + + /** + * Determine if the primary WINS server address has been set + * + * @return boolean + */ + public final boolean hasPrimaryWINSServer() + { + return m_winsPrimary != null ? true : false; + } + + /** + * Return the primary WINS server address + * + * @return InetAddress + */ + public final InetAddress getPrimaryWINSServer() + { + return m_winsPrimary; + } + + /** + * Determine if the secondary WINS server address has been set + * + * @return boolean + */ + public final boolean hasSecondaryWINSServer() + { + return m_winsSecondary != null ? true : false; + } + + /** + * Return the secondary WINS server address + * + * @return InetAddress + */ + public final InetAddress getSecondaryWINSServer() + { + return m_winsSecondary; + } + + /** + * Add a NetBIOS name. + * + * @param name NetBIOS name to be added + * @exception java.io.IOException I/O error occurred. + */ + public final synchronized void AddName(NetBIOSName name) throws IOException + { + + // Check if the NetBIOS name socket has been initialized + + if (m_socket == null) + throw new IOException("NetBIOS name socket not initialized"); + + // Create an add name request and add to the request list + + NetBIOSRequest nbReq = new NetBIOSRequest(NetBIOSRequest.AddName, name, getNextTransactionId()); + + // Set the retry interval + + if (hasPrimaryWINSServer()) + nbReq.setRetryInterval(AddNameWINSInterval); + else + nbReq.setRetryInterval(AddNameInterval); + + // Add the name to the local name list + + m_localNames.addElement(name); + + // Queue the request + + synchronized (m_reqList) + { + + // Add the request to the list + + m_reqList.addElement(nbReq); + + // Wakeup the processing thread + + m_reqList.notify(); + } + } + + /** + * Delete a NetBIOS name. + * + * @param name NetBIOS name to be deleted + * @exception java.io.IOException I/O error occurred. + */ + public final synchronized void DeleteName(NetBIOSName name) throws IOException + { + + // Check if the NetBIOS name socket has been initialized + + if (m_socket == null) + throw new IOException("NetBIOS name socket not initialized"); + + // Create a delete name request and add to the request list + + NetBIOSRequest nbReq = new NetBIOSRequest(NetBIOSRequest.DeleteName, name, getNextTransactionId(), + DeleteNameRetries); + nbReq.setRetryInterval(DeleteNameInterval); + + synchronized (m_reqList) + { + + // Add the request to the list + + m_reqList.addElement(nbReq); + + // Wakeup the processing thread + + m_reqList.notify(); + } + } + + /** + * Add a local add name listener to the NetBIOS name server. + * + * @param l AddNameListener + */ + public final synchronized void addAddNameListener(AddNameListener l) + { + + // Check if the add name listener list is allocated + + if (m_addListeners == null) + m_addListeners = new Vector(); + m_addListeners.addElement(l); + } + + /** + * Add a query name listener to the NetBIOS name server. + * + * @param l QueryNameListener + */ + public final synchronized void addQueryListener(QueryNameListener l) + { + + // Check if the query name listener list is allocated + + if (m_queryListeners == null) + m_queryListeners = new Vector(); + m_queryListeners.addElement(l); + } + + /** + * Add a remote name listener to the NetBIOS name server. + * + * @param l RemoteNameListener + */ + public final synchronized void addRemoteListener(RemoteNameListener l) + { + + // Check if the remote name listener list is allocated + + if (m_remoteListeners == null) + m_remoteListeners = new Vector(); + m_remoteListeners.addElement(l); + } + + /** + * Trigger an add name event to all registered listeners. + * + * @param name NetBIOSName + * @param sts int + */ + protected final synchronized void fireAddNameEvent(NetBIOSName name, int sts) + { + + // Check if there are any listeners + + if (m_addListeners == null || m_addListeners.size() == 0) + return; + + // Create a NetBIOS name event + + NetBIOSNameEvent evt = new NetBIOSNameEvent(name, sts); + + // Inform all registered listeners + + for (int i = 0; i < m_addListeners.size(); i++) + { + AddNameListener addListener = m_addListeners.get(i); + addListener.netbiosNameAdded(evt); + } + } + + /** + * Trigger an query name event to all registered listeners. + * + * @param name NetBIOSName + * @param sts int + */ + protected final synchronized void fireQueryNameEvent(NetBIOSName name, InetAddress addr) + { + + // Check if there are any listeners + + if (m_queryListeners == null || m_queryListeners.size() == 0) + return; + + // Create a NetBIOS name event + + NetBIOSNameEvent evt = new NetBIOSNameEvent(name, NetBIOSNameEvent.QUERY_NAME); + + // Inform all registered listeners + + for (int i = 0; i < m_queryListeners.size(); i++) + { + QueryNameListener queryListener = m_queryListeners.get(i); + queryListener.netbiosNameQuery(evt, addr); + } + } + + /** + * Trigger a name register event to all registered listeners. + * + * @param name NetBIOSName + * @param sts int + */ + protected final synchronized void fireNameRegisterEvent(NetBIOSName name, InetAddress addr) + { + + // Check if there are any listeners + + if (m_remoteListeners == null || m_remoteListeners.size() == 0) + return; + + // Create a NetBIOS name event + + NetBIOSNameEvent evt = new NetBIOSNameEvent(name, NetBIOSNameEvent.REGISTER_NAME); + + // Inform all registered listeners + + for (int i = 0; i < m_remoteListeners.size(); i++) + { + RemoteNameListener nameListener = m_remoteListeners.get(i); + nameListener.netbiosAddRemoteName(evt, addr); + } + } + + /** + * Trigger a name release event to all registered listeners. + * + * @param name NetBIOSName + * @param sts int + */ + protected final synchronized void fireNameReleaseEvent(NetBIOSName name, InetAddress addr) + { + + // Check if there are any listeners + + if (m_remoteListeners == null || m_remoteListeners.size() == 0) + return; + + // Create a NetBIOS name event + + NetBIOSNameEvent evt = new NetBIOSNameEvent(name, NetBIOSNameEvent.REGISTER_NAME); + + // Inform all registered listeners + + for (int i = 0; i < m_remoteListeners.size(); i++) + { + RemoteNameListener nameListener = m_remoteListeners.get(i); + nameListener.netbiosReleaseRemoteName(evt, addr); + } + } + + /** + * Open the server socket + * + * @exception SocketException + */ + private void openSocket() throws java.net.SocketException + { + + // Check if the server should bind to a particular local address, or all addresses + + if (hasBindAddress()) + m_socket = new DatagramSocket(getPort(), m_bindAddress); + else + m_socket = new DatagramSocket(getPort()); + } + + /** + * Process a NetBIOS name query. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processNameQuery(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + + // Check that the name query packet is valid + + if (pkt.getQuestionCount() != 1) + return; + + // Get the name that is being queried + + String searchName = pkt.getQuestionName(); + char nameType = searchName.charAt(15); + + int len = 0; + while (len <= 14 && searchName.charAt(len) != ' ') + len++; + searchName = searchName.substring(0, len); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Query name=" + searchName + ", type=" + NetBIOSName.TypeAsString(nameType) + ", len=" + + len); + + // Search for the name in the local name table + + Enumeration enm = m_localNames.elements(); + NetBIOSName nbName = null; + boolean foundName = false; + + while (enm.hasMoreElements() && foundName == false) + { + + // Get the current NetBIOS name item from the local name table + + nbName = enm.nextElement(); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("NetBIOS Name - " + nbName.getName() + ", len=" + nbName.getName().length() + ",type=" + + NetBIOSName.TypeAsString(nbName.getType())); + + // Check if the name matches the query name + + if (nbName.getType() == nameType && nbName.getName().compareTo(searchName) == 0) + foundName = true; + } + + // Check if we found a matching name + + if (foundName == true) + { + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Found name " + searchName + " in local name table : " + nbName.toString()); + + // Build the name query response + + int pktLen = pkt.buildNameQueryResponse(nbName); + + // Debug + + if (logger.isDebugEnabled()) + { + logger.debug("%% NetBIOS Reply to " + fromAddr.getHostAddress() + " :-"); + pkt.DumpPacket(false); + } + + // Send the reply packet + + try + { + + // Send the name query reply + + sendPacket(pkt, pktLen, fromAddr, fromPort); + } + catch (java.io.IOException ex) + { + logger.error("Name query response error", ex); + } + + // Inform listeners of the name query + + fireQueryNameEvent(nbName, fromAddr); + } + else + { + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Failed to find match for name " + searchName); + } + } + + /** + * Process a NetBIOS name register request. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processNameRegister(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + + // Check that the name register packet is valid + + if (pkt.getQuestionCount() != 1) + return; + + // Get the name that is being registered + + String regName = pkt.getQuestionName(); + char nameType = regName.charAt(15); + + int len = 0; + while (len <= 14 && regName.charAt(len) != ' ') + len++; + regName = regName.substring(0, len); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Register name=" + regName + ", type=" + NetBIOSName.TypeAsString(nameType) + ", len=" + + len); + + // Create a NetBIOS name for the host + + byte[] hostIP = fromAddr.getAddress(); + NetBIOSName nbName = new NetBIOSName(regName, nameType, false, hostIP); + + // Add the name to the remote host name table + + m_remoteNames.put(nbName, hostIP); + + // Inform listeners that a new remote name has been added + + fireNameRegisterEvent(nbName, fromAddr); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Added remote name " + nbName.toString() + " to remote names table"); + } + + /** + * Process a NetBIOS name release. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processNameRelease(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + + // Check that the name release packet is valid + + if (pkt.getQuestionCount() != 1) + return; + + // Get the name that is being released + + String regName = pkt.getQuestionName(); + char nameType = regName.charAt(15); + + int len = 0; + while (len <= 14 && regName.charAt(len) != ' ') + len++; + regName = regName.substring(0, len); + + // Debug + + if (logger.isDebugEnabled()) + logger + .debug("%% Release name=" + regName + ", type=" + NetBIOSName.TypeAsString(nameType) + ", len=" + + len); + + // Create a NetBIOS name for the host + + byte[] hostIP = fromAddr.getAddress(); + NetBIOSName nbName = new NetBIOSName(regName, nameType, false, hostIP); + + // Remove the name from the remote host name table + + m_remoteNames.remove(nbName); + + // Inform listeners that a remote name has been released + + fireNameReleaseEvent(nbName, fromAddr); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Released remote name " + nbName.toString() + " from remote names table"); + } + + /** + * Process a NetBIOS query response. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processQueryResponse(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + } + + /** + * Process a NetBIOS name register response. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processRegisterResponse(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + + // Check if there are any reply name details + + if (pkt.getAnswerCount() == 0) + return; + + // Get the details from the response packet + + int tranId = pkt.getTransactionId(); + + // Find the matching request + + NetBIOSRequest req = findRequest(tranId); + if (req == null) + return; + + // Get the error code from the response + + int errCode = pkt.getResultCode(); + + if (errCode != 0) + { + + // Mark the request error + + req.setErrorStatus(true); + + // Get the name details + + String regName = pkt.getAnswerName(); + char nameType = regName.charAt(15); + + int len = 0; + while (len <= 14 && regName.charAt(len) != ' ') + len++; + regName = regName.substring(0, len); + + // Create a NetBIOS name for the host + + byte[] hostIP = fromAddr.getAddress(); + NetBIOSName nbName = new NetBIOSName(regName, nameType, false, hostIP); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Negative Name Registration name=" + nbName); + + // Inform listeners of the add name failure + + fireAddNameEvent(req.getNetBIOSName(), NetBIOSNameEvent.ADD_FAILED); + } + else + { + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("%% Name Registration Successful name=" + req.getNetBIOSName().getName()); + + // Inform listeners that the add name was successful + + fireAddNameEvent(req.getNetBIOSName(), NetBIOSNameEvent.ADD_SUCCESS); + } + } + + /** + * Process a NetBIOS name release response. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processReleaseResponse(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + } + + /** + * Process a NetBIOS WACK. + * + * @param pkt NetBIOSPacket + * @param fromAddr InetAddress + * @param fromPort int + */ + protected final void processWack(NetBIOSPacket pkt, InetAddress fromAddr, int fromPort) + { + } + + /** + * Remove a local add name listener from the NetBIOS name server. + * + * @param l AddNameListener + */ + public final synchronized void removeAddNameListener(AddNameListener l) + { + + // Check if the listener list is valid + + if (m_addListeners == null) + return; + m_addListeners.removeElement(l); + } + + /** + * Remove a query name listner from the NetBIOS name server. + * + * @param l QueryNameListener + */ + public final synchronized void removeQueryNameListener(QueryNameListener l) + { + + // Check if the listener list is valid + + if (m_queryListeners == null) + return; + m_queryListeners.removeElement(l); + } + + /** + * Remove a remote name listener from the NetBIOS name server. + * + * @param l RemoteNameListener + */ + public final synchronized void removeRemoteListener(RemoteNameListener l) + { + + // Check if the listener list is valid + + if (m_remoteListeners == null) + return; + m_remoteListeners.removeElement(l); + } + + /** + * Run the NetBIOS name server. + */ + public void run() + { + + // Initialize the NetBIOS name socket + + NetBIOSPacket nbPkt = null; + DatagramPacket pkt = null; + byte[] buf = null; + + try + { + + // Get a list of the local IP addresses + + Vector ipList = new Vector(); + + if (hasBindAddress()) + { + + // Use the specified bind address + + ipList.add(getBindAddress().getAddress()); + } + else + { + + // Get a list of all the local addresses + + InetAddress[] addrs = InetAddress.getAllByName(InetAddress.getLocalHost().getHostName()); + + for (int i = 0; i < addrs.length; i++) + { + + // Check for a valid address, filter out '127.0.0.1' and '0.0.0.0' addresses + + if (addrs[i].getHostAddress().equals("127.0.0.1") == false + && addrs[i].getHostAddress().equals("0.0.0.0") == false) + ipList.add(addrs[i].getAddress()); + } + } + + // Initialize the NetBIOS name socket + + if (m_socket == null) + openSocket(); + + // Allocate the NetBIOS request queue, and add the server name/alias name requests + + m_reqList = new Vector(); + + // Add the server name requests to the queue + + AddName(new NetBIOSName(getConfiguration().getServerName(), NetBIOSName.FileServer, false, ipList, + DefaultTTL)); + AddName(new NetBIOSName(getConfiguration().getServerName(), NetBIOSName.WorkStation, false, ipList, + DefaultTTL)); + + if (getConfiguration().getDomainName() != null) + AddName(new NetBIOSName(getConfiguration().getDomainName(), NetBIOSName.Domain, true, ipList, + DefaultTTL)); + + // Create the request handler thread + + m_reqHandler = new NetBIOSRequestHandler(); + m_reqHandler.start(); + + // Create the name refresh thread + + m_refreshThread = new NetBIOSNameRefresh(); + m_refreshThread.start(); + + // Allocate a receive buffer, NetBIOS packet and datagram packet + + buf = new byte[1024]; + nbPkt = new NetBIOSPacket(buf); + pkt = new DatagramPacket(buf, buf.length); + } + catch (Exception ex) + { + + // Debug + + logger.error("NetBIOSNameServer setup error:", ex); + + // Save the exception and inform listeners of the error + + setException(ex); + fireServerEvent(ServerListener.ServerError); + } + + // If there are any pending requests in the queue then wakeup the request handler thread + + if (m_reqList != null && m_reqList.size() > 0) + { + synchronized (m_reqList) + { + m_reqList.notify(); + } + } + + // Indicate that the server is active + + setActive(true); + fireServerEvent(ServerListener.ServerActive); + + // Loop + + if (hasException() == false) + { + + // Clear the shutdown request flag + + m_shutdown = false; + + while (m_shutdown == false) + { + + try + { + + // Wait for an incoming packet .... + + m_socket.receive(pkt); + + // Check for a zero length datagram + + if (pkt.getLength() == 0) + continue; + + // Get the incoming NetBIOS packet opcode + + InetAddress fromAddr = pkt.getAddress(); + int fromPort = pkt.getPort(); + + switch (nbPkt.getOpcode()) + { + + // Name query + + case NetBIOSPacket.NAME_QUERY: + processNameQuery(nbPkt, fromAddr, fromPort); + break; + + // Name register + + case NetBIOSPacket.NAME_REGISTER: + processNameRegister(nbPkt, fromAddr, fromPort); + break; + + // Name release + + case NetBIOSPacket.NAME_RELEASE: + processNameRelease(nbPkt, fromAddr, fromPort); + break; + + // Name register response + + case NetBIOSPacket.RESP_REGISTER: + processRegisterResponse(nbPkt, fromAddr, fromPort); + break; + + // Name query response + + case NetBIOSPacket.RESP_QUERY: + processQueryResponse(nbPkt, fromAddr, fromPort); + break; + + // Name release response + + case NetBIOSPacket.RESP_RELEASE: + processReleaseResponse(nbPkt, fromAddr, fromPort); + break; + + // WACK + + case NetBIOSPacket.WACK: + processWack(nbPkt, fromAddr, fromPort); + break; + + // Refresh + + case NetBIOSPacket.REFRESH: + processNameRegister(nbPkt, fromAddr, fromPort); + break; + + // Multi-homed name registration + + case NetBIOSPacket.NAME_REGISTER_MULTI: + processNameRegister(nbPkt, fromAddr, fromPort); + break; + + // Unknown opcode + + default: + logger.error("Unknown OpCode 0x" + Integer.toHexString(nbPkt.getOpcode())); + break; + } + } + catch (Exception ex) + { + + // Debug + + if ( m_shutdown == false) + logger.error("NetBIOSNameServer error", ex); + + // Store the error and inform listeners of the server error. If the server is + // shutting down we expect a + // socket error as the socket is closed by the shutdown thread and the pending + // read request generates an + // exception. + + if (m_shutdown == false) + { + setException(ex); + fireServerEvent(ServerListener.ServerError); + } + } + } + } + + // Indicate that the server is closed + + setActive(false); + fireServerEvent(ServerListener.ServerShutdown); + } + + /** + * Send a packet via the NetBIOS naming datagram socket. + * + * @param pkt NetBIOSPacket + * @param len int + * @exception java.io.IOException The exception description. + */ + protected final void sendPacket(NetBIOSPacket nbpkt, int len) throws java.io.IOException + { + + // Allocate the datagram packet, using the add name buffer + + DatagramPacket pkt = new DatagramPacket(nbpkt.getBuffer(), len, NetworkSettings.getBroadcastAddress(), + getPort()); + + // Send the datagram packet + + m_socket.send(pkt); + } + + /** + * Send a packet via the NetBIOS naming datagram socket. + * + * @param pkt NetBIOSPacket + * @param len int + * @param replyAddr InetAddress + * @param replyPort int + * @exception java.io.IOException The exception description. + */ + protected final void sendPacket(NetBIOSPacket nbpkt, int len, InetAddress replyAddr, int replyPort) + throws java.io.IOException + { + + // Allocate the datagram packet, using the add name buffer + + DatagramPacket pkt = new DatagramPacket(nbpkt.getBuffer(), len, replyAddr, replyPort); + + // Send the datagram packet + + m_socket.send(pkt); + } + + /** + * Set the local address that the server should bind to + * + * @param addr java.net.InetAddress + */ + public final void setBindAddress(InetAddress addr) + { + m_bindAddress = addr; + } + + /** + * Set the server port + * + * @param port int + */ + public final void setServerPort(int port) + { + m_port = port; + } + + /** + * Set the primary WINS server address + * + * @param addr InetAddress + */ + public final void setPrimaryWINSServer(InetAddress addr) + { + m_winsPrimary = addr; + } + + /** + * Set the secondary WINS server address + * + * @param addr InetAddress + */ + public final void setSecondaryWINSServer(InetAddress addr) + { + m_winsSecondary = addr; + } + + /** + * Find the NetBIOS request with the specified transation id + * + * @param id int + * @return NetBIOSRequest + */ + private final NetBIOSRequest findRequest(int id) + { + + // Check if the request list is valid + + if (m_reqList == null) + return null; + + // Need to lock access to the request list + + NetBIOSRequest req = null; + + synchronized (m_reqList) + { + + // Search for the required request + + int idx = 0; + + while (req == null && idx < m_reqList.size()) + { + + // Get the current request and check if it is the required request + + NetBIOSRequest curReq = (NetBIOSRequest) m_reqList.elementAt(idx++); + if (curReq.getTransactionId() == id) + req = curReq; + } + } + + // Return the request, or null if not found + + return req; + } + + /** + * Shutdown the NetBIOS name server + * + * @param immediate boolean + */ + public void shutdownServer(boolean immediate) + { + + // Close the name refresh thread + + try + { + + if (m_refreshThread != null) + { + m_refreshThread.shutdownRequest(); + } + } + catch (Exception ex) + { + + // Debug + + logger.error("Shutdown NetBIOS server error", ex); + } + + // If the shutdown is not immediate then release all of the names registered by this server + + if (isActive() && immediate == false) + { + + // Release all local names + + for (int i = 0; i < m_localNames.size(); i++) + { + + // Get the current name details + + NetBIOSName nbName = (NetBIOSName) m_localNames.elementAt(i); + + // Queue a delete name request + + try + { + DeleteName(nbName); + } + catch (IOException ex) + { + logger.error("Shutdown NetBIOS server error", ex); + } + } + + // Wait for the request handler thread to process the delete name requests + + while (m_reqList.size() > 0) + { + try + { + Thread.sleep(100); + } + catch (InterruptedException ex) + { + } + } + } + + // Close the request handler thread + + try + { + + // Close the request handler thread + + if (m_reqHandler != null) + { + m_reqHandler.shutdownRequest(); + m_reqHandler.join(1000); + m_reqHandler = null; + } + } + catch (Exception ex) + { + + // Debug + + logger.error("Shutdown NetBIOS request handler error", ex); + } + + // Indicate that the server is closing + + m_shutdown = true; + + try + { + + // Close the server socket so that any pending receive is cancelled + + if (m_socket != null) + { + + try + { + m_socket.close(); + } + catch (Exception ex) + { + } + m_socket = null; + } + } + catch (Exception ex) + { + logger.error("Shutdown NetBIOS server error", ex); + } + + // Fire a shutdown notification event + + fireServerEvent(ServerListener.ServerShutdown); + } + + /** + * Start the NetBIOS name server is a seperate thread + */ + public void startServer() + { + + // Create a seperate thread to run the NetBIOS name server + + m_srvThread = new Thread(this); + m_srvThread.setName("NetBIOS Name Server"); + m_srvThread.setDaemon(true); + + m_srvThread.start(); + + // Fire a server startup event + + fireServerEvent(ServerListener.ServerStartup); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/server/NetBIOSRequest.java b/source/java/org/alfresco/filesys/netbios/server/NetBIOSRequest.java new file mode 100644 index 0000000000..f77df2beca --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/NetBIOSRequest.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +import org.alfresco.filesys.netbios.NetBIOSName; + +/** + * NetBIOS Request Class + *

    + * Contains the details of NetBIOS server request, such as an add name request. + */ +class NetBIOSRequest +{ + + // Request types + + public final static int AddName = 0; + public final static int DeleteName = 1; + public final static int RefreshName = 2; + + // Default retry count and interval + + public final static int DefaultRetries = 5; + public final static long DefaultInterval = 2000; // ms + + // Requets type strings + + private final static String[] _typeNames = { "AddName", "DelName", "RefreshName" }; + + // Request type + + private int m_type; + + // NetBIOS name details + + private NetBIOSName m_nbName; + + // Retry count and interval + + private int m_retry; + private long m_retryIntvl; + + // Response status + + private boolean m_error; + + // Transaction id for this request + + private int m_tranId; + + /** + * Class constructor + * + * @param typ int + * @param nbName NetBIOSName + * @param tranId int + */ + public NetBIOSRequest(int typ, NetBIOSName nbName, int tranId) + { + m_type = typ; + m_nbName = nbName; + m_tranId = tranId; + + m_retry = DefaultRetries; + m_retryIntvl = DefaultInterval; + + m_error = false; + } + + /** + * Class constructor + * + * @param typ int + * @param nbName NetBIOSName + * @param tranId int + * @param retry int + */ + public NetBIOSRequest(int typ, NetBIOSName nbName, int tranId, int retry) + { + m_type = typ; + m_nbName = nbName; + m_tranId = tranId; + + m_retry = retry; + m_retryIntvl = DefaultInterval; + + m_error = false; + } + + /** + * Return the request type + * + * @return int + */ + public final int isType() + { + return m_type; + } + + /** + * Return the type as a string + * + * @return String + */ + public final String getTypeAsString() + { + if (m_type < 0 || m_type >= _typeNames.length) + return ""; + return _typeNames[m_type]; + } + + /** + * Return the NetBIOS name details + * + * @return NetBIOSName + */ + public final NetBIOSName getNetBIOSName() + { + return m_nbName; + } + + /** + * Return the retry count + * + * @return int + */ + public final int getRetryCount() + { + return m_retry; + } + + /** + * Return the retry interval + * + * @return long + */ + public final long getRetryInterval() + { + return m_retryIntvl; + } + + /** + * Return the transaction id + * + * @return int + */ + public final int getTransactionId() + { + return m_tranId; + } + + /** + * Check if the request has an error status + * + * @return boolean + */ + public final boolean hasErrorStatus() + { + return m_error; + } + + /** + * Decrement the retry count + * + * @return int + */ + protected final int decrementRetryCount() + { + return m_retry--; + } + + /** + * Set the error status + * + * @param sts boolean + */ + protected final void setErrorStatus(boolean sts) + { + m_error = sts; + } + + /** + * Set the request retry count + * + * @param retry int + */ + public final void setRetryCount(int retry) + { + m_retry = retry; + } + + /** + * Set the retry interval, in milliseconds + * + * @param interval long + */ + public final void setRetryInterval(long interval) + { + m_retryIntvl = interval; + } + + /** + * Return the request as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getTypeAsString()); + str.append(":"); + str.append(getNetBIOSName()); + str.append(","); + str.append(getRetryCount()); + str.append(","); + str.append(getRetryInterval()); + str.append(","); + str.append(getTransactionId()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/server/PacketReceiver.java b/source/java/org/alfresco/filesys/netbios/server/PacketReceiver.java new file mode 100644 index 0000000000..bdbe7f0a67 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/PacketReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +import java.io.IOException; +import java.net.DatagramSocket; + +/** + * Interface for NetBIOS packet receivers. + */ +public interface PacketReceiver +{ + + /** + * Receive packets on the specified datagram socket. + * + * @param sock java.net.DatagramSocket + * @exception java.io.IOException The exception description. + */ + void ReceivePacket(DatagramSocket sock) throws IOException; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/server/QueryNameListener.java b/source/java/org/alfresco/filesys/netbios/server/QueryNameListener.java new file mode 100644 index 0000000000..d7fd240620 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/QueryNameListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +import java.net.InetAddress; + +/** + * NetBIOS name query listener interface. + */ +public interface QueryNameListener +{ + + /** + * Signal that a NetBIOS name query has been received, for the specified local NetBIOS name. + * + * @param evt Local NetBIOS name details. + * @param addr IP address of the remote node that sent the name query request. + */ + public void netbiosNameQuery(NetBIOSNameEvent evt, InetAddress addr); +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/server/RemoteNameListener.java b/source/java/org/alfresco/filesys/netbios/server/RemoteNameListener.java new file mode 100644 index 0000000000..97b62a7554 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/server/RemoteNameListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.server; + +import java.net.InetAddress; + +/** + * NetBIOS remote name listener interface. + */ +public interface RemoteNameListener +{ + + /** + * Signal that a remote host has added a new NetBIOS name. + * + * @param evt NetBIOSNameEvent + * @param addr java.net.InetAddress + */ + public void netbiosAddRemoteName(NetBIOSNameEvent evt, InetAddress addr); + + /** + * Signal that a remote host has released a NetBIOS name. + * + * @param evt NetBIOSNameEvent + * @param addr java.net.InetAddress + */ + public void netbiosReleaseRemoteName(NetBIOSNameEvent evt, InetAddress addr); +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/netbios/win32/NetBIOS.java b/source/java/org/alfresco/filesys/netbios/win32/NetBIOS.java new file mode 100644 index 0000000000..8265c7eadd --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/win32/NetBIOS.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.win32; + +/** + * NetBIOS API Constants Class + */ +public class NetBIOS +{ + // NetBIOS command codes + + public final static int NCBCall = 0x10; + public final static int NCBListen = 0x11; + public final static int NCBHangup = 0x12; + public final static int NCBSend = 0x14; + public final static int NCBRecv = 0x15; + public final static int NCBRecvAny = 0x16; + public final static int NCBChainSend = 0x17; + public final static int NCBDGSend = 0x20; + public final static int NCBDGRecv = 0x21; + public final static int NCBDGSendBc = 0x22; + public final static int NCBDGRecvBc = 0x23; + public final static int NCBAddName = 0x30; + public final static int NCBDelName = 0x31; + public final static int NCBReset = 0x32; + public final static int NCBAStat = 0x33; + public final static int NCBSStat = 0x34; + public final static int NCBCancel = 0x35; + public final static int NCBAddGrName = 0x36; + public final static int NCBEnum = 0x37; + public final static int NCBUnlink = 0x70; + public final static int NCBSendNA = 0x71; + public final static int NCBChainSendNA = 0x72; + public final static int NCBLANStAlert = 0x73; + public final static int NCBAction = 0x77; + public final static int NCBFindName = 0x78; + public final static int NCBTrace = 0x79; + + public final static int Asynch = 0x80; + + // Status codes + + public final static int NRC_GoodRet = 0x00; + public final static int NRC_BufLen = 0x01; + public final static int NRC_IllCmd = 0x03; + public final static int NRC_CmdTmo = 0x05; + public final static int NRC_Incomp = 0x06; + public final static int NRC_Baddr = 0x07; + public final static int NRC_SNumOut = 0x08; + public final static int NRC_NoRes = 0x09; + public final static int NRC_SClosed = 0x0A; + public final static int NRC_CmdCan = 0x0B; + public final static int NRC_DupName = 0x0D; + public final static int NRC_NamTFul = 0x0E; + public final static int NRC_ActSes = 0x0F; + public final static int NRC_LocTFul = 0x11; + public final static int NRC_RemTFul = 0x12; + public final static int NRC_IllNN = 0x13; + public final static int NRC_NoCall = 0x14; + public final static int NRC_NoWild = 0x15; + public final static int NRC_InUse = 0x16; + public final static int NRC_NamErr = 0x17; + public final static int NRC_SAbort = 0x18; + public final static int NRC_NamConf = 0x19; + public final static int NRC_IfBusy = 0x21; + public final static int NRC_TooMany = 0x22; + public final static int NRC_Bridge = 0x23; + public final static int NRC_CanOccr = 0x24; + public final static int NRC_Cancel = 0x26; + public final static int NRC_DupEnv = 0x30; + public final static int NRC_EnvNotDef = 0x34; + public final static int NRC_OSResNotAv = 0x35; + public final static int NRC_MaxApps = 0x36; + public final static int NRC_NoSaps = 0x37; + public final static int NRC_NoResources = 0x38; + public final static int NRC_InvAddress = 0x39; + public final static int NRC_InvDDid = 0x3B; + public final static int NRC_LockFail = 0x3C; + public final static int NRC_OpenErr = 0x3F; + public final static int NRC_System = 0x40; + public final static int NRC_Pending = 0xFF; + + // Various constants + + public final static int NCBNameSize = 16; + public final static int MaxLANA = 254; + + public final static int NameFlagsMask = 0x87; + + public final static int GroupName = 0x80; + public final static int UniqueName = 0x00; + public final static int Registering = 0x00; + public final static int Registered = 0x04; + public final static int Deregistered = 0x05; + public final static int Duplicate = 0x06; + public final static int DuplicateDereg = 0x07; + public final static int ListenOutstanding = 0x01; + public final static int CallPending = 0x02; + public final static int SessionEstablished = 0x03; + public final static int HangupPending = 0x04; + public final static int HangupComplete = 0x05; + public final static int SessionAborted = 0x06; + + public final static String AllTransports = "M\0\0\0"; + + // Maximum receive size (16bits) + // + // Multiple receives must be issued to receive data packets over this size + + public final static int MaxReceiveSize = 0xFFFF; + + /** + * Return the status string for a NetBIOS error code + * + * @param nbError int + * @return String + */ + public final static String getErrorString(int nbError) + { + + String str = ""; + + switch (nbError) + { + case NRC_GoodRet: + str = "Success status"; + break; + case NRC_BufLen: + str = "Illegal buffer length"; + break; + case NRC_IllCmd: + str = "Illegal command"; + break; + case NRC_CmdTmo: + str = "Command timed out"; + break; + case NRC_Incomp: + str = "Message incomplete, issue another command"; + break; + case NRC_Baddr: + str = "Illegal buffer address"; + break; + case NRC_SNumOut: + str = "Session number out of range"; + break; + case NRC_NoRes: + str = "No resource available"; + break; + case NRC_SClosed: + str = "Session closed"; + break; + case NRC_CmdCan: + str = "Command cancelled"; + break; + case NRC_DupName: + str = "Duplicate name"; + break; + case NRC_NamTFul: + str = "Name table full"; + break; + case NRC_ActSes: + str = "No deletions, name has active sessions"; + break; + case NRC_LocTFul: + str = "Local session table full"; + break; + case NRC_RemTFul: + str = "Remote session table full"; + break; + case NRC_IllNN: + str = "Illegal name number"; + break; + case NRC_NoCall: + str = "No callname"; + break; + case NRC_NoWild: + str = "Cannot put * in ncb_name"; + break; + case NRC_InUse: + str = "Name in use on remote adapter"; + break; + case NRC_NamErr: + str = "Name deleted"; + break; + case NRC_SAbort: + str = "Session ended abnormally"; + break; + case NRC_NamConf: + str = "Name conflict detected"; + break; + case NRC_IfBusy: + str = "Interface busy, IRET before retrying"; + break; + case NRC_TooMany: + str = "Too many commands outstanding, try later"; + break; + case NRC_Bridge: + str = "ncb_lana_num field invalid"; + break; + case NRC_CanOccr: + str = "Command completed whilst cancel occurring"; + break; + case NRC_Cancel: + str = "Command not valid to cancel"; + break; + case NRC_DupEnv: + str = "Name defined by another local process"; + break; + case NRC_EnvNotDef: + str = "Environment undefined, RESET required"; + break; + case NRC_OSResNotAv: + str = "Require OS resources exhausted"; + break; + case NRC_MaxApps: + str = "Max number of applications exceeded"; + break; + case NRC_NoSaps: + str = "No saps available for NetBIOS"; + break; + case NRC_NoResources: + str = "Requested resources not available"; + break; + case NRC_InvAddress: + str = "Invalid ncb address or length"; + break; + case NRC_InvDDid: + str = "Ivalid NCB DDID"; + break; + case NRC_LockFail: + str = "Lock of user area failed"; + break; + case NRC_OpenErr: + str = "NetBIOS not loaded"; + break; + case NRC_System: + str = "System error"; + break; + case NRC_Pending: + str = "Asyncrhonous command pending"; + break; + } + + return str; + } +} diff --git a/source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocket.java b/source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocket.java new file mode 100644 index 0000000000..e0706d38f4 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocket.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.win32; + +import org.alfresco.filesys.netbios.NetBIOSName; + +/** + * NetBIOS Socket Class + * + *

    Contains the details of a Winsock NetBIOS socket that was opened using native code. + * + * @author GKSpencer + */ +public class NetBIOSSocket +{ + // Socket types + + private static final int TypeNormal = 0; + private static final int TypeListener = 1; + private static final int TypeDatagram = 2; + + // Flag to indicate if the NetBIOS socket interface has been initialized + + private static boolean _nbSocketInit; + + // NetBIOS LANA that the socket is associated with + + private int m_lana; + + // Socket pointer (Windows SOCKET) + + private int m_socket; + + // NetBIOS name, either listening name or callers name + + private NetBIOSName m_nbName; + + // Socket type + + private int m_socketType; + + /** + * Initialize the Winsock NetBIOS interface + */ + public static final void initializeSockets() + throws WinsockNetBIOSException { + + // Check if the NetBIOS socket interface has been initialized + + if ( _nbSocketInit == false) + { + // Initialize the NetBIOS socket interface + + Win32NetBIOS.InitializeSockets(); + + // Indicate that the NetBIOS socket interface is initialized + + _nbSocketInit = true; + } + } + + /** + * Shutdown the Winsock NetBIOS interface + */ + public static final void shutdownSockets() + { + // Check if the NetBIOS socket interface has been initialized + + if ( _nbSocketInit == true) + { + // Indicate that the NetBIOS socket interface is not initialized + + _nbSocketInit = false; + + // Initialize the NetBIOS socket interface + + Win32NetBIOS.ShutdownSockets(); + } + } + + /** + * Determine if the Winsock NetBIOS interface is initialized + * + * @return boolean + */ + public static final boolean isInitialized() + { + return _nbSocketInit; + } + + /** + * Create a NetBIOS socket to listen for incoming sessions on the specified LANA + * + * @param lana int + * @param nbName NetBIOSName + * @return NetBIOSSocket + * @exception NetBIOSSocketException + * @exception WinsockNetBIOSException + */ + public static final NetBIOSSocket createListenerSocket(int lana, NetBIOSName nbName) + throws WinsockNetBIOSException, NetBIOSSocketException + { + // Initialize the Winsock NetBIOS interface + + initializeSockets(); + + // Create a new NetBIOS socket + + int sockPtr = Win32NetBIOS.CreateSocket(lana); + if ( sockPtr == 0) + throw new NetBIOSSocketException("Failed to create NetBIOS socket"); + + // Bind the socket to a NetBIOS name + + if ( Win32NetBIOS.BindSocket( sockPtr, nbName.getNetBIOSName()) != 0) + throw new NetBIOSSocketException("Failed to bind NetBIOS socket"); + + // Return the NetBIOS socket + + return new NetBIOSSocket(lana, sockPtr, nbName, TypeListener); + } + + /** + * Create a NetBIOS datagram socket to send out mailslot announcements on the specified LANA + * + * @param lana int + * @return NetBIOSSocket + * @exception NetBIOSSocketException + * @exception WinsockNetBIOSException + */ + public static final NetBIOSSocket createDatagramSocket(int lana) + throws WinsockNetBIOSException, NetBIOSSocketException + { + // Initialize the Winsock NetBIOS interface + + initializeSockets(); + + // Create a new NetBIOS socket + + int sockPtr = Win32NetBIOS.CreateDatagramSocket(lana); + if ( sockPtr == 0) + throw new NetBIOSSocketException("Failed to create NetBIOS datagram socket"); + + // Return the NetBIOS socket + + return new NetBIOSSocket(lana, sockPtr, null, TypeDatagram); + } + + /** + * Class constructor + * + * @param lana int + * @param sockPtr int + * @param nbName NetBIOSName + * @param sockerType int + */ + private NetBIOSSocket(int lana, int sockPtr, NetBIOSName nbName, int socketType) + { + m_lana = lana; + m_nbName = nbName; + m_socket = sockPtr; + + m_socketType = socketType; + } + + /** + * Return the NetBIOS LANA the socket is associated with + * + * @return int + */ + public final int getLana() + { + return m_lana; + } + + /** + * Determine if this is a datagram socket + * + * @return boolean + */ + public final boolean isDatagramSocket() + { + return m_socketType == TypeDatagram ? true : false; + } + + /** + * Determine if this is a listener type socket + * + * @return boolean + */ + public final boolean isListener() + { + return m_socketType == TypeListener ? true : false; + } + + /** + * Determine if the socket is valid + * + * @return boolean + */ + public final boolean hasSocket() + { + return m_socket != 0 ? true : false; + } + + /** + * Return the socket pointer + * + * @return int + */ + public final int getSocket() + { + return m_socket; + } + + /** + * Return the NetBIOS name. For a listening socket this is the local name, for a session + * socket this is the remote callers name. + * + * @return NetBIOSName + */ + public final NetBIOSName getName() + { + return m_nbName; + } + + /** + * Write data to the session socket + * + * @param buf byte[] + * @param off int + * @param len int + * @return int + * @exception WinsockNetBIOSException + */ + public final int write(byte[] buf, int off, int len) + throws WinsockNetBIOSException + { + // Check if this is a datagram socket + + if ( isDatagramSocket()) + throw new WinsockNetBIOSException("Write not allowed for datagram socket"); + + return Win32NetBIOS.SendSocket( getSocket(), buf, off, len); + } + + /** + * Read data from the session socket + * + * @param buf byte[] + * @param off int + * @param maxLen int + * @return int + * @exception WinsockNetBIOSException + */ + public final int read(byte[] buf, int off, int maxLen) + throws WinsockNetBIOSException + { + // Check if this is a datagram socket + + if ( isDatagramSocket()) + throw new WinsockNetBIOSException("Read not allowed for datagram socket"); + + return Win32NetBIOS.ReceiveSocket( getSocket(), buf, off, maxLen); + } + + /** + * Send a datagram to a group name + * + * @param toName NetBIOSName + * @param buf byte[] + * @param off int + * @param len int + * @return int + * @exception WinsockNetBIOSException + */ + public final int sendDatagram(NetBIOSName toName, byte[] buf, int off, int len) + throws WinsockNetBIOSException + { + // Check if this is a datagram socket + + if ( isDatagramSocket() == false) + throw new WinsockNetBIOSException("Not a datagram type socket"); + + return Win32NetBIOS.SendSocketDatagram( getSocket(), toName.getNetBIOSName(), buf, off, len); + } + + /** + * Listen for an incoming session connection and create a session socket for the new session + * + * @return NetBIOSSocket + * @exception NetBIOSSocketException + * @exception winsockNetBIOSException + */ + public final NetBIOSSocket listen() + throws WinsockNetBIOSException, NetBIOSSocketException + { + // Check if this socket is a listener socket, and the socket is valid + + if ( isListener() == false) + throw new NetBIOSSocketException("Not a listener type socket"); + + if ( hasSocket() == false) + throw new NetBIOSSocketException("NetBIOS socket not valid"); + + // Wait for an incoming session request + + byte[] callerName = new byte[NetBIOSName.NameLength]; + + int sessSockPtr = Win32NetBIOS.ListenSocket( getSocket(), callerName); + if ( sessSockPtr == 0) + throw new NetBIOSSocketException("NetBIOS socket listen failed"); + + // Return the new NetBIOS socket session + + return new NetBIOSSocket(getLana(), sessSockPtr, new NetBIOSName(callerName, 0), TypeNormal); + } + + /** + * Close the socket + */ + public final void closeSocket() + { + // Close the native socket, if valid + + if ( hasSocket()) + { + Win32NetBIOS.CloseSocket( getSocket()); + setSocket(0); + } + } + + /** + * Set the socket pointer + * + * @param sockPtr int + */ + protected final void setSocket(int sockPtr) + { + m_socket = sockPtr; + } + + /** + * Return the NetBIOS socket details as a string + * + * @return String + */ + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("[LANA:"); + str.append(getLana()); + str.append(",Name:"); + if ( getName() != null) + str.append(getName()); + else + str.append(""); + + str.append(",Socket:"); + if ( hasSocket()) + { + str.append("0x"); + str.append(Integer.toHexString(getSocket())); + } + else + str.append(""); + + switch( m_socketType) + { + case TypeNormal: + str.append("Session"); + break; + case TypeListener: + str.append("Listener"); + break; + case TypeDatagram: + str.append("Datagram"); + break; + } + + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocketException.java b/source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocketException.java new file mode 100644 index 0000000000..56d832e2ce --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/win32/NetBIOSSocketException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.win32; + +/** + * NetBIOS Socket Exception Class + * + * @author GKSpencer + */ +public class NetBIOSSocketException extends Exception +{ + private static final long serialVersionUID = 2363178480979507007L; + + /** + * Class constructor + * + * @param msg String + */ + public NetBIOSSocketException(String msg) + { + super(msg); + } +} diff --git a/source/java/org/alfresco/filesys/netbios/win32/Win32NetBIOS.java b/source/java/org/alfresco/filesys/netbios/win32/Win32NetBIOS.java new file mode 100644 index 0000000000..eec3c4b8f0 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/win32/Win32NetBIOS.java @@ -0,0 +1,713 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.win32; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.Hashtable; + +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.IPAddress; + +/** + * Win32 NetBIOS Native Call Wrapper Class + */ +public class Win32NetBIOS +{ + + // Constants + // + // FIND_NAME_BUFFER structure length + + protected final static int FindNameBufferLen = 33; + + // Exception if the native code DLL load failed + + private static Throwable m_loadDLLException; + + /** + * Check if the native code was loaded successfully + * + * @return boolean + */ + public static final boolean isInitialized() + { + return m_loadDLLException == null ? true : false; + } + + /** + * Return the native code load exception + * + * @return Throwable + */ + public static final Throwable getInitializationException() + { + return m_loadDLLException; + } + + /** + * Check if NetBIOS is enabled on any network adapters + * + * @return boolean + */ + public static final boolean isAvailable() { + + // Check if the DLL was loaded successfully + + if ( isInitialized() == false) + return false; + + // Check if there are any valid LANAs, if not then NetBIOS is not enabled or network + // adapters that have NetBIOS enabled are not currently enabled + + int[] lanas = LanaEnum(); + if ( lanas != null && lanas.length > 0) + return true; + return false; + } + + /** + * Add a NetBIOS name to the local name table + * + * @param lana int + * @param name byte[] + * @return int + */ + public static native int AddName(int lana, byte[] name); + + /** + * Add a group NetBIOS name to the local name table + * + * @param lana int + * @param name byte[] + * @return int + */ + public static native int AddGroupName(int lana, byte[] name); + + /** + * Find a NetBIOS name, return the name buffer + * + * @param lana int + * @param name byte[] + * @param nameBuf byte[] + * @param bufLen int + * @return int + */ + public static native int FindNameRaw(int lana, byte[] name, byte[] nameBuf, int bufLen); + + /** + * Find a NetBIOS name + * + * @param lana int + * @param name NetBIOSName + * @return int + */ + public static int FindName(int lana, NetBIOSName nbName) + { + + // Allocate a buffer to receive the name details + + byte[] nameBuf = new byte[nbName.isGroupName() ? 65535 : 4096]; + + // Get the raw NetBIOS name data + + int sts = FindNameRaw(lana, nbName.getNetBIOSName(), nameBuf, nameBuf.length); + + if (sts != NetBIOS.NRC_GoodRet) + return -sts; + + // Unpack the FIND_NAME_HEADER structure + + DataBuffer buf = new DataBuffer(nameBuf, 0, nameBuf.length); + + int nodeCount = buf.getShort(); + buf.skipBytes(1); + boolean isGroupName = buf.getByte() == 0 ? false : true; + + // Unpack the FIND_NAME_BUFFER structures + + int curPos = buf.getPosition(); + + for (int i = 0; i < nodeCount; i++) + { + + // FIND_NAME_BUFFER: + // UCHAR length + // UCHAR access_control + // UCHAR frame_control + // UCHAR destination_addr[6] + // UCHAR source_addr[6] + // UCHAR routing_info[18] + + // Skip to the source_addr field + + buf.skipBytes(9); + + // Source address field format should be 0.0.n.n.n.n for TCP/IP address + + if (buf.getByte() == 0 && buf.getByte() == 0) + { + + // Looks like a TCP/IP format address, unpack it + + byte[] ipAddr = new byte[4]; + + ipAddr[0] = (byte) buf.getByte(); + ipAddr[1] = (byte) buf.getByte(); + ipAddr[2] = (byte) buf.getByte(); + ipAddr[3] = (byte) buf.getByte(); + + // Add the address to the list of TCP/IP addresses for the NetBIOS name + + nbName.addIPAddress(ipAddr); + + // Skip to the start of the next FIND_NAME_BUFFER structure + + curPos += FindNameBufferLen; + buf.setPosition(curPos); + } + } + + // Return the node count + + return nodeCount; + } + + /** + * Delete a NetBIOS name from the local name table + * + * @param lana int + * @param name byte[] + * @return int + */ + public static native int DeleteName(int lana, byte[] name); + + /** + * Enumerate the available LANAs + * + * @return int[] + */ + public static int[] LanaEnumerate() + { + // Make sure that there is an active network adapter as making calls to the LanaEnum native call + // causes problems when there are no active network adapters. + + boolean adapterAvail = false; + + try + { + // Enumerate the available network adapters and check for an active adapter, not including + // the loopback adapter + + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + + while ( nis.hasMoreElements() && adapterAvail == false) + { + NetworkInterface ni = nis.nextElement(); + if ( ni.getName().equals("lo") == false) + { + // Make sure the adapter has a valid IP address + + Enumeration addrs = ni.getInetAddresses(); + if ( addrs.hasMoreElements()) + adapterAvail = true; + } + } + + } + catch ( SocketException ex) + { + } + + // Check if there are network adapter(s) available + + if ( adapterAvail == false) + return null; + + // Call the native code to return the available LANA list + + return LanaEnum(); + } + + /** + * Enumerate the available LANAs + * + * @return int[] + */ + private static native int[] LanaEnum(); + + /** + * Reset the NetBIOS environment + * + * @param lana int + * @return int + */ + public static native int Reset(int lana); + + /** + * Listen for an incoming session request + * + * @param lana int + * @param toName byte[] + * @param fromName byte[] + * @param callerName byte[] + * @return int + */ + public static native int Listen(int lana, byte[] toName, byte[] fromName, byte[] callerName); + + /** + * Receive a data packet on a session + * + * @param lana int + * @param lsn int + * @param buf byte[] + * @param off int + * @param maxLen int + * @return int + */ + public static native int Receive(int lana, int lsn, byte[] buf, int off, int maxLen); + + /** + * Send a data packet on a session + * + * @param lana int + * @param lsn int + * @param buf byte[] + * @param off int + * @param len int + * @return int + */ + public static native int Send(int lana, int lsn, byte[] buf, int off, int len); + + /** + * Send a datagram to a specified name + * + * @param lana int + * @param srcNum int + * @param destName byte[] + * @param buf byte[] + * @param off int + * @param len int + * @return int + */ + public static native int SendDatagram(int lana, int srcNum, byte[] destName, byte[] buf, int off, int len); + + /** + * Send a broadcast datagram + * + * @param lana + * @param buf byte[] + * @param off int + * @param len int + * @return int + */ + public static native int SendBroadcastDatagram(int lana, byte[] buf, int off, int len); + + /** + * Receive a datagram on a specified name + * + * @param lana int + * @param nameNum int + * @param buf byte[] + * @param off int + * @param maxLen int + * @return int + */ + public static native int ReceiveDatagram(int lana, int nameNum, byte[] buf, int off, int maxLen); + + /** + * Receive a broadcast datagram + * + * @param lana int + * @param nameNum int + * @param buf byte[] + * @param off int + * @param maxLen int + * @return int + */ + public static native int ReceiveBroadcastDatagram(int lana, int nameNum, byte[] buf, int off, int maxLen); + + /** + * Hangup a session + * + * @param lsn int + * @return int + */ + public static native int Hangup(int lana, int lsn); + + /** + * Return the local computers NetBIOS name + * + * @return String + */ + public static native String GetLocalNetBIOSName(); + + /** + * Return the local domain name + * + * @return String + */ + public static native String GetLocalDomainName(); + + /** + * Return a comma delimeted list of WINS server TCP/IP addresses, or null if no WINS servers are + * configured. + * + * @return String + */ + public static native String getWINSServerList(); + + /** + * Find the TCP/IP address for a LANA + * + * @param lana int + * @return String + */ + public static final String getIPAddressForLANA(int lana) + { + + // Get the local NetBIOS name + + String localName = GetLocalNetBIOSName(); + if (localName == null) + return null; + + // Create a NetBIOS name for the local name + + NetBIOSName nbName = new NetBIOSName(localName, NetBIOSName.WorkStation, false); + + // Get the local NetBIOS name details + + int sts = FindName(lana, nbName); + + if (sts == -NetBIOS.NRC_EnvNotDef) + { + + // Reset the LANA then try the name lookup again + + Reset(lana); + sts = FindName(lana, nbName); + } + + // Check if the name lookup was successful + + String ipAddr = null; + + if (sts >= 0) + { + + // Get the first IP address from the list + + ipAddr = nbName.getIPAddressString(0); + } + + // Return the TCP/IP address for the LANA + + return ipAddr; + } + + /** + * Find the adapter name for a LANA + * + * @param lana int + * @return String + */ + public static final String getAdapterNameForLANA(int lana) + { + + // Get the TCP/IP address for a LANA + + String ipAddr = getIPAddressForLANA(lana); + if (ipAddr == null) + return null; + + // Get the list of available network adapters + + Hashtable adapters = getNetworkAdapterList(); + String adapterName = null; + + if (adapters != null) + { + + // Find the network adapter for the TCP/IP address + + NetworkInterface ni = adapters.get(ipAddr); + if (ni != null) + adapterName = ni.getDisplayName(); + } + + // Return the adapter name for the LANA + + return adapterName; + } + + /** + * Find the LANA for a TCP/IP address + * + * @param addr String + * @return int + */ + public static final int getLANAForIPAddress(String addr) + { + + // Check if the address is a numeric TCP/IP address + + if (IPAddress.isNumericAddress(addr) == false) + return -1; + + // Get a list of the available NetBIOS LANAs + + int[] lanas = LanaEnum(); + if (lanas == null || lanas.length == 0) + return -1; + + // Search for the LANA with the matching TCP/IP address + + for (int i = 0; i < lanas.length; i++) + { + + // Get the current LANAs TCP/IP address + + String curAddr = getIPAddressForLANA(lanas[i]); + if (curAddr != null && curAddr.equals(addr)) + return lanas[i]; + } + + // Failed to find the LANA for the specified TCP/IP address + + return -1; + } + + /** + * Find the LANA for a network adapter + * + * @param name String + * @return int + */ + public static final int getLANAForAdapterName(String name) + { + + // Get the list of available network adapters + + Hashtable niList = getNetworkAdapterList(); + + // Search for the address of the specified network adapter + + Enumeration niEnum = niList.keys(); + + while (niEnum.hasMoreElements()) + { + + // Get the current TCP/IP address + + String ipAddr = niEnum.nextElement(); + NetworkInterface ni = niList.get(ipAddr); + + if (ni.getDisplayName().equalsIgnoreCase(name)) + { + + // Return the LANA for the network adapters TCP/IP address + + return getLANAForIPAddress(ipAddr); + } + } + + // Failed to find matching network adapter + + return -1; + } + + /** + * Return a hashtable of NetworkInterfaces indexed by TCP/IP address + * + * @return Hashtable + */ + private static final Hashtable getNetworkAdapterList() + { + + // Get a list of the local network adapters + + Hashtable niList = new Hashtable(); + + try + { + + // Enumerate the available network adapters + + Enumeration niEnum = NetworkInterface.getNetworkInterfaces(); + + while (niEnum.hasMoreElements()) + { + + // Get the current network interface details + + NetworkInterface ni = niEnum.nextElement(); + Enumeration addrEnum = ni.getInetAddresses(); + + while (addrEnum.hasMoreElements()) + { + + // Get the address and add the adapter to the list indexed via the numeric IP + // address string + + InetAddress addr = addrEnum.nextElement(); + niList.put(addr.getHostAddress(), ni); + } + } + } + catch (Exception ex) + { + } + + // Return the network adapter list + + return niList; + } + + //---------- Winsock based NetBIOS interface ----------// + + /** + * Initialize the NetBIOS socket interface + * + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native void InitializeSockets() + throws WinsockNetBIOSException; + + /** + * Shutdown the NetBIOS socket interface + */ + protected static native void ShutdownSockets(); + + /** + * Create a NetBIOS socket + * + * @param lana int + * @return int + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int CreateSocket(int lana) + throws WinsockNetBIOSException; + + /** + * Create a NetBIOS datagram socket + * + * @param lana int + * @return int + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int CreateDatagramSocket(int lana) + throws WinsockNetBIOSException; + + /** + * Bind a NetBIOS socket to a name to listen for incoming sessions + * + * @param sockPtr int + * @param name byte[] + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int BindSocket(int sockPtr, byte[] name) + throws WinsockNetBIOSException; + + /** + * Listen for an incoming connection + * + * @param sockPtr int + * @param callerName byte[] + * @return int + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int ListenSocket(int sockPtr, byte[] callerName) + throws WinsockNetBIOSException; + + /** + * Close a NetBIOS socket + * + * @param sockPtr int + */ + protected static native void CloseSocket(int sockPtr); + + /** + * Send data on a session socket + * + * @param sockPtr int + * @param buf byte[] + * @param off int + * @param len int + * @return int + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int SendSocket(int sockPtr, byte[] buf, int off, int len) + throws WinsockNetBIOSException; + + /** + * Receive data on a session socket + * + * @param sockPtr int + * @param toName byte[] + * @param buf byte[] + * @param off int + * @param maxLen int + * @return int + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int ReceiveSocket(int sockPtr, byte[] buf, int off, int maxLen) + throws WinsockNetBIOSException; + + /** + * Send data on a datagram socket + * + * @param sockPtr int + * @param toName byte[] + * @param buf byte[] + * @param off int + * @param len int + * @return int + * @exception WinsockNetBIOSException If a Winsock error occurs + */ + protected static native int SendSocketDatagram(int sockPtr, byte[] toName, byte[] buf, int off, int len) + throws WinsockNetBIOSException; + + /** + * Wait for a network address change event, block until a change occurs or the Winsock NetBIOS + * interface is shut down + */ + public static native void waitForNetworkAddressChange(); + + /** + * Static initializer used to load the native code library + */ + static + { + + // Load the Win32 NetBIOS interface library + + try + { + System.loadLibrary("Win32NetBIOS"); + } + catch (Throwable ex) + { + // Save the native code load exception + + m_loadDLLException = ex; + } + } +} diff --git a/source/java/org/alfresco/filesys/netbios/win32/WinsockError.java b/source/java/org/alfresco/filesys/netbios/win32/WinsockError.java new file mode 100644 index 0000000000..18fbaeaff6 --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/win32/WinsockError.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.win32; + +/** + * Winsock Error Codes Class + * + *

    Contains a list of the error codes that the Win32 Winsock calls may generate, and a method to convert + * to an error text string. + * + * @author GKSpencer + */ +public class WinsockError +{ + // Winsock error code constants + + public static final int WsaEIntr = 10004; + public static final int WsaEAcces = 10013; + public static final int WsaEFault = 10014; + public static final int WsaEInval = 10022; + public static final int WsaEMfile = 10024; + public static final int WsaEWouldBlock = 10035; + public static final int WsaEInProgress = 10036; + public static final int WsaEAlready = 10037; + public static final int WsaENotSock = 10038; + public static final int WsaEDestAddrReq = 10039; + public static final int WsaEMsgSize = 10040; + public static final int WsaEPrototype = 10041; + public static final int WsaENoProtoOpt = 10042; + public static final int WsaEProtoNoSupp = 10043; + public static final int WsaESocktNoSupp = 10044; + public static final int WsaEOpNotSupp = 10045; + public static final int WsaEPFNoSupport = 10046; + public static final int WsaEAFNoSupport = 10047; + public static final int WsaEAddrInUse = 10048; + public static final int WsaEAddrNotAvail= 10049; + public static final int WsaENetDown = 10050; + public static final int WsaENetUnReach = 10051; + public static final int WsaENetReset = 10052; + public static final int WsaEConnAborted = 10053; + public static final int WsaEConnReset = 10054; + public static final int WsaENoBufs = 10055; + public static final int WsaEIsConn = 10056; + public static final int WsaENotConn = 10057; + public static final int WsaEShutdown = 10058; + public static final int WsaETimedout = 10060; + public static final int WsaEConnRefused = 10061; + public static final int WsaEHostDown = 10064; + public static final int WsaEHostUnreach = 10065; + public static final int WsaEProcLim = 10067; + public static final int WsaSysNotReady = 10091; + public static final int WsaVerNotSupp = 10092; + public static final int WsaNotInit = 10093; + public static final int WsaEDiscon = 10101; + public static final int WsaTypeNotFound = 10109; + public static final int WsaHostNotFound = 11001; + public static final int WsaTryAgain = 11002; + public static final int WsaNoRecovery = 11003; + public static final int WsaNoData = 11004; + + /** + * Convert a Winsock error code to a text string + * + * @param sts int + * @return String + */ + public static final String asString(int sts) + { + String errText = null; + + switch ( sts) + { + case WsaEIntr: + errText = "Interrupted function call"; + break; + case WsaEAcces: + errText = "Permission denied"; + break; + case WsaEFault: + errText = "Bad address"; + break; + case WsaEInval: + errText = "Invalid argument"; + break; + case WsaEMfile: + errText = "Too many open files"; + break; + case WsaEWouldBlock: + errText = "Resource temporarily unavailable"; + break; + case WsaEInProgress: + errText = "Operation now in progress"; + break; + case WsaEAlready: + errText = "Operation already in progress"; + break; + case WsaENotSock: + errText = "Socket operation on nonsocket"; + break; + case WsaEDestAddrReq: + errText = "Destination address required"; + break; + case WsaEMsgSize: + errText = "Message too long"; + break; + case WsaEPrototype: + errText = "Protocol wrong type for socket"; + break; + case WsaENoProtoOpt: + errText = "Bad protocol option"; + break; + case WsaEProtoNoSupp: + errText = "Protocol not supported"; + break; + case WsaESocktNoSupp: + errText = "Socket type not supported"; + break; + case WsaEOpNotSupp: + errText = "Operation not supported"; + break; + case WsaEPFNoSupport: + errText = "Protocol family not supported"; + break; + case WsaEAFNoSupport: + errText = "Address family not supported by protocol family"; + break; + case WsaEAddrInUse: + errText = "Address already in use"; + break; + case WsaEAddrNotAvail: + errText = "Cannot assign requested address"; + break; + case WsaENetDown: + errText = "Network is down"; + break; + case WsaENetUnReach: + errText = "Network is unreachable"; + break; + case WsaENetReset: + errText = "Network dropped connection on reset"; + break; + case WsaEConnAborted: + errText = "Software caused connection abort"; + break; + case WsaEConnReset: + errText = "Connection reset by peer"; + break; + case WsaENoBufs: + errText = "No buffer space available"; + break; + case WsaEIsConn: + errText = "Socket is already connected"; + break; + case WsaENotConn: + errText = "Socket is not connected"; + break; + case WsaEShutdown: + errText = "Cannot send after socket shutdown"; + break; + case WsaETimedout: + errText = "Connection timed out"; + break; + case WsaEConnRefused: + errText = "Connection refused"; + break; + case WsaEHostDown: + errText = "Host is down"; + break; + case WsaEHostUnreach: + errText = "No route to host"; + break; + case WsaEProcLim: + errText = "Too many processes"; + break; + case WsaSysNotReady: + errText = "Network subsystem is unavailable"; + break; + case WsaVerNotSupp: + errText = "Winsock.dll version out of range"; + break; + case WsaNotInit: + errText = "Successful WSAStartup not yet performed"; + break; + case WsaEDiscon: + errText = "Graceful shutdown in progress"; + break; + case WsaTypeNotFound: + errText = "Class type not found"; + break; + case WsaHostNotFound: + errText = "Host not found"; + break; + case WsaTryAgain: + errText = "Nonauthoritative host not found"; + break; + case WsaNoRecovery: + errText = "This is a nonrecoverable error"; + break; + case WsaNoData: + errText = "Valid name, no data record of requested type"; + break; + default: + errText = "Unknown Winsock error 0x" + Integer.toHexString(sts); + break; + } + + return errText; + } +} diff --git a/source/java/org/alfresco/filesys/netbios/win32/WinsockNetBIOSException.java b/source/java/org/alfresco/filesys/netbios/win32/WinsockNetBIOSException.java new file mode 100644 index 0000000000..bb4973d2fb --- /dev/null +++ b/source/java/org/alfresco/filesys/netbios/win32/WinsockNetBIOSException.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.netbios.win32; + +import java.io.IOException; + +/** + * Winsock NetBIOS Exception Class + * + *

    Contains the Winsock error code from the failed Winsock call. + * + * @author GKSpencer + */ +public class WinsockNetBIOSException extends IOException +{ + private static final long serialVersionUID = 5933702607108016674L; + + // Winsock error code + + private int m_errCode; + + /** + * Default constructor + */ + public WinsockNetBIOSException() + { + super(); + } + + /** + * Class constructor + * + * @param msg String + */ + public WinsockNetBIOSException(String msg) + { + super(msg); + + // Split out the error code + + if ( msg != null) + { + int pos = msg.indexOf(":"); + if ( pos != -1) + m_errCode = Integer.valueOf(msg.substring(0, pos)); + } + } + + /** + * Class constructor + * + * @param sts int + */ + public WinsockNetBIOSException(int sts) + { + super(); + + m_errCode = sts; + } + + /** + * Return the Winsock error code + * + * @return int + */ + public final int getErrorCode() + { + return m_errCode; + } + + /** + * Set the error code + * + * @param sts int + */ + public final void setErrorCode(int sts) + { + m_errCode = sts; + } + + /** + * Return the error message string + * + * @return String + */ + public String getMessage() + { + StringBuilder msg = new StringBuilder(); + + msg.append( super.getMessage()); + String winsockErr = WinsockError.asString(getErrorCode()); + if ( winsockErr != null) + { + msg.append(" - "); + msg.append(winsockErr); + } + + return msg.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/NetworkServer.java b/source/java/org/alfresco/filesys/server/NetworkServer.java new file mode 100644 index 0000000000..e07482f6f7 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/NetworkServer.java @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server; + +import java.net.InetAddress; +import java.util.Vector; + +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.auth.acl.AccessControlManager; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.ShareMapper; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Network Server Base Class + *

    + * Base class for server implementations for different protocols. + */ +public abstract class NetworkServer +{ + private static final Log logger = LogFactory.getLog("org.alfresco.filesys"); + + // Protocol name + + private String m_protoName; + + // Server version + + private String m_version; + + // Server configuration + private ServerConfiguration m_config; + + // Debug enabled flag and debug flags + + private boolean m_debug; + private int m_debugFlags; + + // List of addresses that the server is bound to + + private InetAddress[] m_ipAddr; + + // Server shutdown flag and server active flag + + private boolean m_shutdown = false; + private boolean m_active = false; + + // Server error exception details + + private Exception m_exception; + + // Server events listener + + private ServerListener m_listener; + + // Session listener list + + private Vector m_sessListeners; + + /** + * Class constructor + * + * @param proto String + * @param config ServerConfiguration + */ + public NetworkServer(String proto, ServerConfiguration config) + { + m_protoName = proto; + m_config = config; + } + + /** + * Returns the server configuration. + * + * @return ServerConfiguration + */ + public final ServerConfiguration getConfiguration() + { + return m_config; + } + + /** + * Return the authenticator for this server + * + * @return SrvAuthenticator + */ + public final SrvAuthenticator getAuthenticator() + { + return getConfiguration().getAuthenticator(); + } + + /** + * Determine if an access control manager is configured + * + * @return boolean + */ + public final boolean hasAccessControlManager() + { + return getConfiguration().getAccessControlManager() != null ? true : false; + } + + /** + * Return the access control manager + * + * @return AccessControlManager + */ + public final AccessControlManager getAccessControlManager() + { + return getConfiguration().getAccessControlManager(); + } + + /** + * Return the main server name + * + * @return String + */ + public final String getServerName() + { + return m_config.getServerName(); + } + + /** + * Return the list of IP addresses that the server is bound to. + * + * @return java.net.InetAddress[] + */ + public final InetAddress[] getServerAddresses() + { + return m_ipAddr; + } + + /** + * Return the share mapper + * + * @return ShareMapper + */ + public final ShareMapper getShareMapper() + { + return m_config.getShareMapper(); + } + + /** + * Return the available shared device list. + * + * @param host String + * @param sess SrvSession + * @return SharedDeviceList + */ + public final SharedDeviceList getShareList(String host, SrvSession sess) + { + return getConfiguration().getShareMapper().getShareList(host, sess, false); + } + + /** + * Return the complete shared device list. + * + * @param host String + * @param sess SrvSession + * @return SharedDeviceList + */ + public final SharedDeviceList getFullShareList(String host, SrvSession sess) + { + return getConfiguration().getShareMapper().getShareList(host, sess, true); + } + + /** + * Find the shared device with the specified name. + * + * @param host Host name from the UNC path + * @param name Name of the shared device to find. + * @param typ Shared device type + * @param sess Session details + * @param create Create share flag, false indicates lookup only + * @return SharedDevice with the specified name and type, else null. + * @exception Exception + */ + public final SharedDevice findShare(String host, String name, int typ, SrvSession sess, boolean create) + throws Exception + { + + // Search for the specified share + + SharedDevice dev = getConfiguration().getShareMapper().findShare(host, name, typ, sess, create); + + // Return the shared device, or null + + return dev; + } + + /** + * Determine if the SMB server is active. + * + * @return boolean + */ + public final boolean isActive() + { + return m_active; + } + + /** + * Return the server version string, in 'n.n.n' format + * + * @return String + */ + + public final String isVersion() + { + return m_version; + } + + /** + * Check if there is a stored server exception + * + * @return boolean + */ + public final boolean hasException() + { + return m_exception != null ? true : false; + } + + /** + * Return the stored exception + * + * @return Exception + */ + public final Exception getException() + { + return m_exception; + } + + /** + * Clear the stored server exception + */ + public final void clearException() + { + m_exception = null; + } + + /** + * Return the server protocol name + * + * @return String + */ + public final String getProtocolName() + { + return m_protoName; + } + + /** + * Determine if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Determine if the specified debug flag is enabled + * + * @return boolean + */ + public final boolean hasDebugFlag(int flg) + { + return (m_debugFlags & flg) != 0 ? true : false; + } + + /** + * Check if the shutdown flag is set + * + * @return boolean + */ + public final boolean hasShutdown() + { + return m_shutdown; + } + + /** + * Set/clear the server active flag + * + * @param active boolean + */ + protected void setActive(boolean active) + { + m_active = active; + } + + /** + * Set the stored server exception + * + * @param ex Exception + */ + protected final void setException(Exception ex) + { + m_exception = ex; + } + + /** + * Set the addresses that the server is bound to + * + * @param adds InetAddress[] + */ + protected final void setServerAddresses(InetAddress[] addrs) + { + m_ipAddr = addrs; + } + + /** + * Set the server version + * + * @param ver String + */ + protected final void setVersion(String ver) + { + m_version = ver; + } + + /** + * Enable/disable debug output for the server + * + * @param dbg boolean + */ + protected final void setDebug(boolean dbg) + { + m_debug = dbg; + } + + /** + * Set the debug flags + * + * @param flags int + */ + protected final void setDebugFlags(int flags) + { + m_debugFlags = flags; + setDebug(flags == 0 ? false : true); + } + + /** + * Set/clear the shutdown flag + * + * @param ena boolean + */ + protected final void setShutdown(boolean ena) + { + m_shutdown = ena; + } + + /** + * Add a server listener to this server + * + * @param l ServerListener + */ + public final void addServerListener(ServerListener l) + { + m_listener = l; + } + + /** + * Remove the server listener + * + * @param l ServerListener + */ + public final void removeServerListener(ServerListener l) + { + if (m_listener == l) + m_listener = null; + } + + /** + * Add a new session listener to the network server. + * + * @param l SessionListener + */ + public final void addSessionListener(SessionListener l) + { + + // Check if the session listener list is allocated + + if (m_sessListeners == null) + m_sessListeners = new Vector(); + m_sessListeners.add(l); + } + + /** + * Remove a session listener from the network server. + * + * @param l SessionListener + */ + public final void removeSessionListener(SessionListener l) + { + + // Check if the listener list is valid + + if (m_sessListeners == null) + return; + m_sessListeners.removeElement(l); + } + + /** + * Fire a server event to the registered listener + * + * @param event int + */ + protected final void fireServerEvent(int event) + { + + // Check if there is a listener registered with this server + + if (m_listener != null) + { + try + { + m_listener.serverStatusEvent(this, event); + } + catch (Exception ex) + { + } + } + } + + /** + * Start the network server + */ + public abstract void startServer(); + + /** + * Shutdown the network server + * + * @param immediate boolean + */ + public abstract void shutdownServer(boolean immediate); + + /** + * Trigger a closed session event to all registered session listeners. + * + * @param sess SrvSession + */ + protected final void fireSessionClosedEvent(SrvSession sess) + { + + // Check if there are any listeners + + if (m_sessListeners == null || m_sessListeners.size() == 0) + return; + + // Inform all registered listeners + + for (int i = 0; i < m_sessListeners.size(); i++) + { + + // Get the current session listener + + try + { + SessionListener sessListener = (SessionListener) m_sessListeners.elementAt(i); + sessListener.sessionClosed(sess); + } + catch (Exception ex) + { + logger.error("Session listener error [closed]: ", ex); + } + } + } + + /** + * Trigger a new session event to all registered session listeners. + * + * @param sess SrvSession + */ + protected final void fireSessionLoggedOnEvent(SrvSession sess) + { + + // Check if there are any listeners + + if (m_sessListeners == null || m_sessListeners.size() == 0) + return; + + // Inform all registered listeners + + for (int i = 0; i < m_sessListeners.size(); i++) + { + + // Get the current session listener + + try + { + SessionListener sessListener = (SessionListener) m_sessListeners.elementAt(i); + sessListener.sessionLoggedOn(sess); + } + catch (Exception ex) + { + logger.error("Session listener error [logon]: ", ex); + } + } + } + + /** + * Trigger a new session event to all registered session listeners. + * + * @param sess SrvSession + */ + protected final void fireSessionOpenEvent(SrvSession sess) + { + + // Check if there are any listeners + + if (m_sessListeners == null || m_sessListeners.size() == 0) + return; + + // Inform all registered listeners + + for (int i = 0; i < m_sessListeners.size(); i++) + { + + // Get the current session listener + + try + { + SessionListener sessListener = (SessionListener) m_sessListeners.elementAt(i); + sessListener.sessionCreated(sess); + } + catch (Exception ex) + { + logger.error("Session listener error [open]: ", ex); + } + } + } +} diff --git a/source/java/org/alfresco/filesys/server/NetworkServerList.java b/source/java/org/alfresco/filesys/server/NetworkServerList.java new file mode 100644 index 0000000000..ee2e9d4e6a --- /dev/null +++ b/source/java/org/alfresco/filesys/server/NetworkServerList.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server; + +import java.util.Vector; + +/** + * Network Server List Class + */ +public class NetworkServerList +{ + // List of network servers + + private Vector m_servers; + + /** + * Class constructor + */ + public NetworkServerList() + { + m_servers = new Vector(); + } + + /** + * Return the number of servers in the list + * + * @return int + */ + public final int numberOfServers() + { + return m_servers.size(); + } + + /** + * Add a server to the list + * + * @param server NetworkServer + */ + public final void addServer(NetworkServer server) + { + m_servers.add(server); + } + + /** + * Return the specified server + * + * @param idx int + * @return NetworkServer + */ + public final NetworkServer getServer(int idx) + { + + // Range check the index + + if (idx < 0 || idx >= m_servers.size()) + return null; + return m_servers.get(idx); + } + + /** + * Find a server in the list by name + * + * @param name String + * @return NetworkServer + */ + public final NetworkServer findServer(String name) + { + + // Search for the required server + + for (int i = 0; i < m_servers.size(); i++) + { + + // Get the current server from the list + + NetworkServer server = m_servers.get(i); + + if (server.getProtocolName().equals(name)) + return server; + } + + // Server not found + + return null; + } + + /** + * Remove the server at the specified position within the list + * + * @param idx int + * @return NetworkServer + */ + public final NetworkServer removeServer(int idx) + { + + // Range check the index + + if (idx < 0 || idx >= m_servers.size()) + return null; + + // Remove the server from the list + + NetworkServer server = m_servers.get(idx); + m_servers.remove(idx); + return server; + } + + /** + * Remove the server with the specified protocol name + * + * @param proto String + * @return NetworkServer + */ + public final NetworkServer removeServer(String proto) + { + + // Search for the required server + + for (int i = 0; i < m_servers.size(); i++) + { + + // Get the current server from the list + + NetworkServer server = m_servers.get(i); + + if (server.getProtocolName().equals(proto)) + { + m_servers.remove(i); + return server; + } + } + + // Server not found + + return null; + } + + /** + * Remove all servers from the list + */ + public final void removeAll() + { + m_servers.removeAllElements(); + } +} diff --git a/source/java/org/alfresco/filesys/server/ServerListener.java b/source/java/org/alfresco/filesys/server/ServerListener.java new file mode 100644 index 0000000000..76208e8780 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/ServerListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server; + +/** + * Server Listener Interface + *

    + * The server listener allows external components to receive notification of server startup, + * shutdown and error events. + */ +public interface ServerListener +{ + // Server event types + + public static final int ServerStartup = 0; + public static final int ServerActive = 1; + public static final int ServerShutdown = 2; + public static final int ServerError = 3; + + /** + * Receive a server event notification + * + * @param server NetworkServer + * @param event int + */ + public void serverStatusEvent(NetworkServer server, int event); +} diff --git a/source/java/org/alfresco/filesys/server/SessionListener.java b/source/java/org/alfresco/filesys/server/SessionListener.java new file mode 100644 index 0000000000..ab3fa0b92a --- /dev/null +++ b/source/java/org/alfresco/filesys/server/SessionListener.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server; + +/** + *

    + * The session listener interface provides a hook into the server so that an application is notified + * when a new session is created and closed by a network server. + */ +public interface SessionListener +{ + + /** + * Called when a network session is closed. + * + * @param sess Network session details. + */ + public void sessionClosed(SrvSession sess); + + /** + * Called when a new network session is created by a network server. + * + * @param sess Network session that has been created for the new connection. + */ + public void sessionCreated(SrvSession sess); + + /** + * Called when a user logs on to a network server + * + * @param sess Network session that has been logged on. + */ + public void sessionLoggedOn(SrvSession sess); +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/SrvSession.java b/source/java/org/alfresco/filesys/server/SrvSession.java new file mode 100644 index 0000000000..e480f6b95a --- /dev/null +++ b/source/java/org/alfresco/filesys/server/SrvSession.java @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server; + +import java.net.InetAddress; + +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.alfresco.service.transaction.TransactionService; + +/** + * Server Session Base Class + *

    + * Base class for server session implementations for different protocols. + */ +public abstract class SrvSession +{ + + // Network server this session is associated with + + private NetworkServer m_server; + + // Session id/slot number + + private int m_sessId; + + // Unique session id string + + private String m_uniqueId; + + // Process id + + private int m_processId = -1; + + // Session/user is logged on/validated + + private boolean m_loggedOn; + + // Client details + + private ClientInfo m_clientInfo; + + // Challenge key used for this session + + private byte[] m_challenge; + + // Debug flags for this session + + private int m_debug; + private String m_dbgPrefix; + + // Session shutdown flag + + private boolean m_shutdown; + + // Protocol type + + private String m_protocol; + + // Remote client/host name + + private String m_remoteName; + + // Authentication token, used during logon + + private Object m_authToken; + + // List of dynamic/temporary shares created for this session + + private SharedDeviceList m_dynamicShares; + + // Active transaction and read/write flag + + private UserTransaction m_transaction; + private boolean m_readOnlyTrans; + + // Request and transaction counts + + protected int m_reqCount; + protected int m_transCount; + protected int m_transConvCount; + + /** + * Class constructor + * + * @param sessId int + * @param srv NetworkServer + * @param proto String + * @param remName String + */ + public SrvSession(int sessId, NetworkServer srv, String proto, String remName) + { + m_sessId = sessId; + m_server = srv; + + setProtocolName(proto); + setRemoteName(remName); + } + + /** + * Add a dynamic share to the list of shares created for this session + * + * @param shrDev SharedDevice + */ + public final void addDynamicShare(SharedDevice shrDev) { + + // Check if the dynamic share list must be allocated + + if ( m_dynamicShares == null) + m_dynamicShares = new SharedDeviceList(); + + // Add the new share to the list + + m_dynamicShares.addShare(shrDev); + } + + /** + * Return the authentication token + * + * @return Object + */ + public final Object getAuthenticationToken() + { + return m_authToken; + } + + /** + * Determine if the authentication token is set + * + * @return boolean + */ + public final boolean hasAuthenticationToken() + { + return m_authToken != null ? true : false; + } + + /** + * Return the session challenge key + * + * @return byte[] + */ + public final byte[] getChallengeKey() + { + return m_challenge; + } + + /** + * Determine if the challenge key has been set for this session + * + * @return boolean + */ + public final boolean hasChallengeKey() + { + return m_challenge != null ? true : false; + } + + /** + * Return the process id + * + * @return int + */ + public final int getProcessId() + { + return m_processId; + } + + /** + * Return the remote client network address + * + * @return InetAddress + */ + public abstract InetAddress getRemoteAddress(); + + /** + * Return the session id for this session. + * + * @return int + */ + public final int getSessionId() + { + return m_sessId; + } + + /** + * Return the server this session is associated with + * + * @return NetworkServer + */ + public final NetworkServer getServer() + { + return m_server; + } + + /** + * Check if the session has valid client information + * + * @return boolean + */ + public final boolean hasClientInformation() + { + return m_clientInfo != null ? true : false; + } + + /** + * Return the client information + * + * @return ClientInfo + */ + public final ClientInfo getClientInformation() + { + return m_clientInfo; + } + + /** + * Determine if the session has any dynamic shares + * + * @return boolean + */ + public final boolean hasDynamicShares() { + return m_dynamicShares != null ? true : false; + } + + /** + * Return the list of dynamic shares created for this session + * + * @return SharedDeviceList + */ + public final SharedDeviceList getDynamicShareList() { + return m_dynamicShares; + } + + /** + * Determine if the protocol type has been set + * + * @return boolean + */ + public final boolean hasProtocolName() + { + return m_protocol != null ? true : false; + } + + /** + * Return the protocol name + * + * @return String + */ + public final String getProtocolName() + { + return m_protocol; + } + + /** + * Determine if the remote client name has been set + * + * @return boolean + */ + public final boolean hasRemoteName() + { + return m_remoteName != null ? true : false; + } + + /** + * Return the remote client name + * + * @return String + */ + public final String getRemoteName() + { + return m_remoteName; + } + + /** + * Determine if the session is logged on/validated + * + * @return boolean + */ + public final boolean isLoggedOn() + { + return m_loggedOn; + } + + /** + * Determine if the session has been shut down + * + * @return boolean + */ + public final boolean isShutdown() + { + return m_shutdown; + } + + /** + * Return the unique session id + * + * @return String + */ + public final String getUniqueId() + { + return m_uniqueId; + } + + /** + * Determine if the specified debug flag is enabled. + * + * @return boolean + * @param dbg int + */ + public final boolean hasDebug(int dbgFlag) + { + if ((m_debug & dbgFlag) != 0) + return true; + return false; + } + + /** + * Set the authentication token + * + * @param authToken Object + */ + public final void setAuthenticationToken(Object authToken) + { + m_authToken = authToken; + } + + /** + * Set the client information + * + * @param client ClientInfo + */ + public final void setClientInformation(ClientInfo client) + { + m_clientInfo = client; + } + + /** + * Set the session challenge key + * + * @param key byte[] + */ + public final void setChallengeKey(byte[] key) + { + m_challenge = key; + } + + /** + * Set the debug output interface. + * + * @param flgs int + */ + public final void setDebug(int flgs) + { + m_debug = flgs; + } + + /** + * Set the debug output prefix for this session + * + * @param prefix String + */ + public final void setDebugPrefix(String prefix) + { + m_dbgPrefix = prefix; + } + + /** + * Set the logged on/validated status for the session + * + * @param loggedOn boolean + */ + public final void setLoggedOn(boolean loggedOn) + { + m_loggedOn = loggedOn; + } + + /** + * Set the process id + * + * @param id int + */ + public final void setProcessId(int id) + { + m_processId = id; + } + + /** + * Set the protocol name + * + * @param name String + */ + public final void setProtocolName(String name) + { + m_protocol = name; + } + + /** + * Set the remote client name + * + * @param name String + */ + public final void setRemoteName(String name) + { + m_remoteName = name; + } + + /** + * Set the session id for this session. + * + * @param id int + */ + public final void setSessionId(int id) + { + m_sessId = id; + } + + /** + * Set the unique session id + * + * @param unid String + */ + public final void setUniqueId(String unid) + { + m_uniqueId = unid; + } + + /** + * Set the shutdown flag + * + * @param flg boolean + */ + protected final void setShutdown(boolean flg) + { + m_shutdown = flg; + } + + /** + * Close the network session + */ + public void closeSession() + { + // Release any dynamic shares owned by this session + + if ( hasDynamicShares()) { + + // Close the dynamic shares + + getServer().getShareMapper().deleteShares(this); + } + } + + /** + * Create and start a transaction, if not already active + * + * @param transService TransactionService + * @param readOnly boolean + * @return boolean + * @exception AlfrescoRuntimeException + */ + public final boolean beginTransaction(TransactionService transService, boolean readOnly) + throws AlfrescoRuntimeException + { + boolean created = false; + + // If there is an active transaction check that it is the required type + + if ( m_transaction != null) + { + // Check if the transaction is a write transaction, if write has been requested + + if ( readOnly == false && m_readOnlyTrans == true) + { + // Commit the read-only transaction + + try + { + m_transaction.commit(); + m_transConvCount++; + } + catch ( Exception ex) + { + throw new AlfrescoRuntimeException("Failed to commit read-only transaction, " + ex.getMessage()); + } + finally + { + // Clear the active transaction + + m_transaction = null; + } + } + } + + // Create the transaction + + if ( m_transaction == null) + { + try + { + m_transaction = transService.getUserTransaction(readOnly); + m_transaction.begin(); + + created = true; + + m_readOnlyTrans = readOnly; + + m_transCount++; + } + catch (Exception ex) + { + throw new AlfrescoRuntimeException("Failed to create transaction, " + ex.getMessage()); + } + } + + return created; + } + + /** + * Determine if the session has an active transaction + * + * @return boolean + */ + public final boolean hasUserTransaction() + { + return m_transaction != null ? true : false; + } + + /** + * Get the active transaction and clear the stored transaction + * + * @return UserTransaction + */ + public final UserTransaction getUserTransaction() + { + UserTransaction trans = m_transaction; + m_transaction = null; + return trans; + } +} diff --git a/source/java/org/alfresco/filesys/server/SrvSessionList.java b/source/java/org/alfresco/filesys/server/SrvSessionList.java new file mode 100644 index 0000000000..24b91aa932 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/SrvSessionList.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server; + +import java.util.Enumeration; +import java.util.Hashtable; + +/** + * Server Session List Class + */ +public class SrvSessionList +{ + + // Session list + + private Hashtable m_sessions; + + /** + * Class constructor + */ + public SrvSessionList() + { + m_sessions = new Hashtable(); + } + + /** + * Return the number of sessions in the list + * + * @return int + */ + public final int numberOfSessions() + { + return m_sessions.size(); + } + + /** + * Add a session to the list + * + * @param sess SrvSession + */ + public final void addSession(SrvSession sess) + { + m_sessions.put(sess.getSessionId(), sess); + } + + /** + * Find the session using the unique session id + * + * @param id int + * @return SrvSession + */ + public final SrvSession findSession(int id) + { + return findSession(id); + } + + /** + * Find the session using the unique session id + * + * @param id Integer + * @return SrvSession + */ + public final SrvSession findSession(Integer id) + { + return m_sessions.get(id); + } + + /** + * Remove a session from the list + * + * @param id int + * @return SrvSession + */ + public final SrvSession removeSession(int id) + { + return removeSession(new Integer(id)); + } + + /** + * Remove a session from the list + * + * @param sess SrvSession + * @return SrvSession + */ + public final SrvSession removeSession(SrvSession sess) + { + return removeSession(sess.getSessionId()); + } + + /** + * Remove a session from the list + * + * @param id Integer + * @return SrvSession + */ + public final SrvSession removeSession(Integer id) + { + + // Find the required session + + SrvSession sess = findSession(id); + + // Remove the session and return the removed session + + m_sessions.remove(id); + return sess; + } + + /** + * Enumerate the session ids + * + * @return Enumeration + */ + public final Enumeration enumerate() + { + return m_sessions.keys(); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java new file mode 100644 index 0000000000..3d97afb26e --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import javax.transaction.UserTransaction; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.MD4PasswordEncoder; +import org.alfresco.repo.security.authentication.MD4PasswordEncoderImpl; +import org.alfresco.repo.security.authentication.NTLMMode; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Alfresco Authenticator Class + * + *

    The Alfresco authenticator implementation enables user level security mode using the Alfresco authentication + * component. + * + *

    Note: Switching off encrypted password support will cause later NT4 service pack releases and + * Win2000 to refuse to connect to the server without a registry update on the client. + * + * @author GKSpencer + */ +public class AlfrescoAuthenticator extends SrvAuthenticator +{ + // Logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol.auth"); + + // Random number generator used to generate challenge keys + + private Random m_random = new Random(System.currentTimeMillis()); + + // Server configuration + + private ServerConfiguration m_config; + + // Authentication component, used to access internal authentication functions + + private AuthenticationComponent m_authComponent; + + // MD4 hash decoder + + private MD4PasswordEncoder m_md4Encoder = new MD4PasswordEncoderImpl(); + + // Various services required to get user information + + private NodeService m_nodeService; + private PersonService m_personService; + private TransactionService m_transactionService; + + /** + * Default Constructor + * + *

    Default to user mode security with encrypted password support. + */ + public AlfrescoAuthenticator() + { + setAccessMode(SrvAuthenticator.USER_MODE); + setEncryptedPasswords(true); + } + + /** + * Authenticate the connection to a share + * + * @param client ClienInfo + * @param share SharedDevice + * @param pwd Share level password. + * @param sess Server session + * @return Authentication status. + */ + public int authenticateShareConnect(ClientInfo client, SharedDevice share, String pwd, SrvSession sess) + { + // Allow write access + // + // Main authentication is handled by authenticateUser() + + return SrvAuthenticator.Writeable; + } + + /** + * Authenticate a user + * + * @param client Client information + * @param sess Server session + * @param alg Encryption algorithm + */ + public int authenticateUser(ClientInfo client, SrvSession sess, int alg) + { + // Check if this is an SMB/CIFS null session logon. + // + // The null session will only be allowed to connect to the IPC$ named pipe share. + + if (client.isNullSession() && sess instanceof SMBSrvSession) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Null CIFS logon allowed"); + + return SrvAuthenticator.AUTH_ALLOW; + } + + // Check if the client is already authenticated, and it is not a null logon + + if ( client.getAuthenticationToken() != null && client.getLogonType() != ClientInfo.LogonNull) + { + // Use the existing authentication token + + m_authComponent.setCurrentUser(client.getUserName()); + + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Re-using existing authentication token"); + + // Return the authentication status + + return client.getLogonType() != ClientInfo.LogonGuest ? AUTH_ALLOW : AUTH_GUEST; + } + + // Check if MD4 or passthru mode is configured + + int authSts = AUTH_DISALLOW; + + if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) + { + // Perform local MD4 password check + + authSts = doMD4UserAuthentication(client, sess, alg); + } + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Authenticated user " + client.getUserName() + " sts=" + getStatusAsString(authSts) + + " via " + (m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER ? "MD4" : "Passthru")); + + // Return the authentication status + + return authSts; + } + + /** + * Generate a challenge key + * + * @param sess SrvSession + * @return byte[] + */ + public byte[] getChallengeKey(SrvSession sess) + { + // In MD4 mode we generate the challenge locally + + byte[] key = null; + + // Check if the client is already authenticated, and it is not a null logon + + if ( sess.hasClientInformation() && sess.getClientInformation().getAuthenticationToken() != null && + sess.getClientInformation().getLogonType() != ClientInfo.LogonNull) + { + // Return the previous challenge, user is already authenticated + + key = sess.getChallengeKey(); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Re-using existing challenge, already authenticated"); + } + else if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) + { + // Generate a new challenge key, pack the key and return + + key = new byte[8]; + + DataPacker.putIntelLong(m_random.nextLong(), key, 0); + } + + // Return the challenge + + return key; + } + + /** + * Search for the required user account details in the defined user list + * + * @param user String + * @return UserAccount + */ + public UserAccount getUserDetails(String user) + { + return null; + } + + /** + * Initialize the authenticator + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initialize(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException + { + // Save the server configuration so we can access the authentication component + + m_config = config; + + // Check that the required authentication classes are available + + m_authComponent = m_config.getAuthenticationComponent(); + + if ( m_authComponent == null) + throw new InvalidConfigurationException("Authentication component not available"); + + if ( m_authComponent.getNTLMMode() != NTLMMode.MD4_PROVIDER) + throw new InvalidConfigurationException("Required authentication mode not available"); + + // Get hold of various services + + m_nodeService = config.getNodeService(); + m_personService = config.getPersonService(); + m_transactionService = config.getTransactionService(); + } + + /** + * Perform MD4 user authentication + * + * @param client Client information + * @param sess Server session + * @param alg Encryption algorithm + * @return int + */ + private final int doMD4UserAuthentication(ClientInfo client, SrvSession sess, int alg) + { + // Get the stored MD4 hashed password for the user, or null if the user does not exist + + String md4hash = m_authComponent.getMD4HashedPassword(client.getUserName()); + + if ( md4hash != null) + { + // Check if the client has supplied an NTLM hashed password, if not then do not allow access + + if ( client.getPassword() == null) + return SrvAuthenticator.AUTH_BADPASSWORD; + + try + { + // Generate the local encrypted password using the challenge that was sent to the client + + byte[] p21 = new byte[21]; + byte[] md4byts = m_md4Encoder.decodeHash(md4hash); + System.arraycopy(md4byts, 0, p21, 0, 16); + + // Generate the local hash of the password using the same challenge + + byte[] localHash = getEncryptor().doNTLM1Encryption(p21, sess.getChallengeKey()); + + // Validate the password + + byte[] clientHash = client.getPassword(); + + if ( clientHash == null || clientHash.length != localHash.length) + return SrvAuthenticator.AUTH_BADPASSWORD; + + for ( int i = 0; i < clientHash.length; i++) + { + if ( clientHash[i] != localHash[i]) + return SrvAuthenticator.AUTH_BADPASSWORD; + } + + // Set the current user to be authenticated, save the authentication token + + client.setAuthenticationToken( m_authComponent.setCurrentUser(client.getUserName())); + + // Get the users home folder node, if available + + getHomeFolderForUser( client); + + // Passwords match, grant access + + return SrvAuthenticator.AUTH_ALLOW; + } + catch (NoSuchAlgorithmException ex) + { + } + + // Error during password check, do not allow access + + return SrvAuthenticator.AUTH_DISALLOW; + } + + // Check if this is an SMB/CIFS null session logon. + // + // The null session will only be allowed to connect to the IPC$ named pipe share. + + if (client.isNullSession() && sess instanceof SMBSrvSession) + return SrvAuthenticator.AUTH_ALLOW; + + // User does not exist, check if guest access is allowed + + return allowGuest() ? SrvAuthenticator.AUTH_GUEST : SrvAuthenticator.AUTH_DISALLOW; + } + + /** + * Get the home folder for the user + * + * @param client ClientInfo + */ + private final void getHomeFolderForUser(ClientInfo client) + { + // Get the home folder for the user + + UserTransaction tx = m_transactionService.getUserTransaction(); + NodeRef homeSpaceRef = null; + + try + { + tx.begin(); + homeSpaceRef = (NodeRef) m_nodeService.getProperty(m_personService.getPerson(client.getUserName()), + ContentModel.PROP_HOMEFOLDER); + client.setHomeFolder( homeSpaceRef); + tx.commit(); + } + catch (Exception ex) + { + try + { + tx.rollback(); + } + catch (Exception ex2) + { + logger.error("Failed to rollback transaction", ex); + } + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/ClientInfo.java b/source/java/org/alfresco/filesys/server/auth/ClientInfo.java new file mode 100644 index 0000000000..6cce583fe9 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/ClientInfo.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import org.alfresco.service.cmr.repository.NodeRef; + +import net.sf.acegisecurity.Authentication; + +/** + * Client Information Class + * + *

    The client information class holds the details of a remote user from a session setup or tree + * connect request. + * + * @author GKSpencer + */ +public class ClientInfo +{ + + // Logon types + + public final static int LogonNormal = 0; + public final static int LogonGuest = 1; + public final static int LogonNull = 2; + public final static int LogonAdmin = 3; + + // Logon type strings + + private static final String[] _logonTypStr = { "Normal", "Guest", "Null", "Administrator" }; + + // User name and password + + private String m_user; + private byte[] m_password; + + // ANSI encrypted password + + private byte[] m_ansiPwd; + + // Logon type + + private int m_logonType; + + // User's domain + + private String m_domain; + + // Operating system type + + private String m_opsys; + + // Remote network address + + private String m_ipAddr; + + // Authentication token + + private Authentication m_authToken; + + // Home folder node + + private NodeRef m_homeNode; + + /** + * Class constructor + * + * @param user User name + * @param pwd Password + */ + public ClientInfo(String user, byte[] pwd) + { + setUserName(user); + setPassword(pwd); + } + + /** + * Get the remote users domain. + * + * @return String + */ + public final String getDomain() + { + return m_domain; + } + + /** + * Get the remote operating system + * + * @return String + */ + public final String getOperatingSystem() + { + return m_opsys; + } + + /** + * Get the password. + * + * @return String. + */ + public final byte[] getPassword() + { + return m_password; + } + + /** + * Return the password as a string + * + * @return String + */ + public final String getPasswordAsString() + { + if (m_password != null) + return new String(m_password); + return null; + } + + /** + * Determine if the client has specified an ANSI password + * + * @return boolean + */ + public final boolean hasANSIPassword() + { + return m_ansiPwd != null ? true : false; + } + + /** + * Return the ANSI encrypted password + * + * @return byte[] + */ + public final byte[] getANSIPassword() + { + return m_ansiPwd; + } + + /** + * Return the ANSI password as a string + * + * @return String + */ + public final String getANSIPasswordAsString() + { + if (m_ansiPwd != null) + return new String(m_ansiPwd); + return null; + } + + /** + * Get the user name. + * + * @return String + */ + public final String getUserName() + { + return m_user; + } + + /** + * Return the logon type + * + * @return int + */ + public final int getLogonType() + { + return m_logonType; + } + + /** + * Return the logon type as a string + * + * @return String + */ + public final String getLogonTypeString() + { + return _logonTypStr[m_logonType]; + } + + /** + * Determine if the user is logged on as a guest + * + * @return boolean + */ + public final boolean isGuest() + { + return m_logonType == LogonGuest ? true : false; + } + + /** + * Determine if the session is a null session + * + * @return boolean + */ + public final boolean isNullSession() + { + return m_logonType == LogonNull ? true : false; + } + + /** + * Determine if the user if logged on as an administrator + * + * @return boolean + */ + public final boolean isAdministrator() + { + return m_logonType == LogonAdmin ? true : false; + } + + /** + * Determine if the client network address has been set + * + * @return boolean + */ + public final boolean hasClientAddress() + { + return m_ipAddr != null ? true : false; + } + + /** + * Return the client network address + * + * @return String + */ + public final String getClientAddress() + { + return m_ipAddr; + } + + /** + * Check if the client has an authentication token + * + * @return boolean + */ + public final boolean hasAuthenticationToken() + { + return m_authToken != null ? true : false; + } + + /** + * Return the authentication token + * + * @return Authentication + */ + public final Authentication getAuthenticationToken() + { + return m_authToken; + } + + /** + * Check if the client has a home folder node + * + * @return boolean + */ + public final boolean hasHomeFolder() + { + return m_homeNode != null ? true : false; + } + + /** + * Return the home folder node + * + * @return NodeRef + */ + public final NodeRef getHomeFolder() + { + return m_homeNode; + } + + /** + * Set the remote users domain + * + * @param domain Remote users domain + */ + public final void setDomain(String domain) + { + m_domain = domain; + } + + /** + * Set the remote users operating system type. + * + * @param opsys Remote operating system + */ + public final void setOperatingSystem(String opsys) + { + m_opsys = opsys; + } + + /** + * Set the password. + * + * @param pwd byte[] + */ + public final void setPassword(byte[] pwd) + { + m_password = pwd; + } + + /** + * Set the ANSI encrypted password + * + * @param pwd byte[] + */ + public final void setANSIPassword(byte[] pwd) + { + m_ansiPwd = pwd; + } + + /** + * Set the password + * + * @param pwd Password string. + */ + public final void setPassword(String pwd) + { + if (pwd != null) + m_password = pwd.getBytes(); + else + m_password = null; + } + + /** + * Set the user name + * + * @param user User name string. + */ + public final void setUserName(String user) + { + m_user = user; + } + + /** + * Set the logon type + * + * @param logonType int + */ + public final void setLogonType(int logonType) + { + m_logonType = logonType; + } + + /** + * Set the guest logon flag + * + * @param guest boolean + */ + public final void setGuest(boolean guest) + { + setLogonType(guest == true ? LogonGuest : LogonNormal); + } + + /** + * Set the client network address + * + * @param addr String + */ + public final void setClientAddress(String addr) + { + m_ipAddr = addr; + } + + /** + * Set the authentication toekn + * + * @param token Authentication + */ + public final void setAuthenticationToken(Authentication token) + { + m_authToken = token; + } + + /** + * Set the home folder node + * + * @param homeNode NodeRef + */ + public final void setHomeFolder(NodeRef homeNode) + { + m_homeNode = homeNode; + } + + /** + * Display the client information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + str.append(getUserName()); + str.append(":"); + str.append(getPassword()); + str.append(","); + str.append(getDomain()); + str.append(","); + str.append(getOperatingSystem()); + + if (hasClientAddress()) + { + str.append(","); + str.append(getClientAddress()); + } + + if ( hasAuthenticationToken()) + { + str.append(",token="); + str.append(getAuthenticationToken()); + } + + if (isGuest()) + str.append(",Guest"); + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/DefaultAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/DefaultAuthenticator.java new file mode 100644 index 0000000000..952f234123 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/DefaultAuthenticator.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + *

    + * Default authenticator class. + *

    + * The default authenticator implementation enables user level security mode and allows any user to + * connect to the server. + */ +public class DefaultAuthenticator extends SrvAuthenticator +{ + + // Server configuration + + private ServerConfiguration m_config; + + /** + * Class constructor + */ + public DefaultAuthenticator() + { + setAccessMode(USER_MODE); + setEncryptedPasswords(true); + } + + /** + * Allow any user to access the server + * + * @param client Client details. + * @param share Shared device the user is connecting to. + * @param pwd Share level password. + * @param sess Server session + * @return int + */ + public int authenticateShareConnect(ClientInfo client, SharedDevice share, String pwd, SrvSession sess) + { + return Writeable; + } + + /** + * Allow any user to access the server. + * + * @param client Client details. + * @param sess Server session + * @param alg Encryption algorithm + * @return int + */ + public int authenticateUser(ClientInfo client, SrvSession sess, int alg) + { + return AUTH_ALLOW; + } + + /** + * The default authenticator does not use encrypted passwords. + * + * @param sess SrvSession + * @return byte[] + */ + public byte[] getChallengeKey(SrvSession sess) + { + return null; + } + + /** + * Search for the requried user account details in the defined user list + * + * @param user String + * @return UserAccount + */ + public UserAccount getUserDetails(String user) + { + + // Get the user account list from the configuration + + UserAccountList userList = m_config.getUserAccounts(); + if (userList == null || userList.numberOfUsers() == 0) + return null; + + // Search for the required user account record + + return userList.findUser(user); + } + + /** + * Initialzie the authenticator + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initialize(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException + { + + // Save the server configuration so we can access the defined user list + + m_config = config; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/InvalidUserException.java b/source/java/org/alfresco/filesys/server/auth/InvalidUserException.java new file mode 100644 index 0000000000..d5bf1c5663 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/InvalidUserException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +/** + * Invalid User Exception Class + */ +public class InvalidUserException extends Exception +{ + private static final long serialVersionUID = 3833743295984645425L; + + /** + * Default constructor. + */ + public InvalidUserException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public InvalidUserException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/LocalAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/LocalAuthenticator.java new file mode 100644 index 0000000000..2a7fc82695 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/LocalAuthenticator.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import java.util.Random; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.alfresco.filesys.util.DataPacker; + +/** + *

    + * Local Authenticator Class. + *

    + * The local authenticator implementation enables user level security mode and uses the user account + * list that is part of the server configuration to determine if a user is allowed to access the + * server/share. + *

    + * Note: Switching off encrypted password support will cause later NT4 service pack releases and + * Win2000 to refuse to connect to the server without a registry update on the client. + */ +public class LocalAuthenticator extends SrvAuthenticator +{ + + // Random number generator used to generate challenge keys + + private Random m_random = new Random(System.currentTimeMillis()); + + // Server configuration + + private ServerConfiguration m_config; + + /** + * Local Authenticator Constructor + *

    + * Default to user mode security with encrypted password support. + */ + public LocalAuthenticator() + { + setAccessMode(SrvAuthenticator.USER_MODE); + setEncryptedPasswords(true); + } + + /** + * Authenticate the connection to a share + * + * @param client ClienInfo + * @param share SharedDevice + * @param pwd Share level password. + * @param sess Server session + * @return Authentication status. + */ + public int authenticateShareConnect(ClientInfo client, SharedDevice share, String pwd, SrvSession sess) + { + + // If the server is in share mode security allow the user access + + if (this.getAccessMode() == SHARE_MODE) + return SrvAuthenticator.Writeable; + + // Check if the IPC$ share is being accessed + + if (share.getType() == ShareType.ADMINPIPE) + return SrvAuthenticator.Writeable; + + // Check if the user is allowed to access the specified shared device + // + // If a user does not have access to the requested share the connection will still be + // allowed + // but any attempts to access files or search directories will result in a 'no access + // rights' + // error being returned to the client. + + UserAccount user = null; + if (client != null) + user = getUserDetails(client.getUserName()); + + if (user == null) + { + + // Check if the guest account is enabled + + return allowGuest() ? SrvAuthenticator.Writeable : SrvAuthenticator.NoAccess; + } + else if (user.hasShare(share.getName()) == false) + return SrvAuthenticator.NoAccess; + + // Allow user to access this share + + return SrvAuthenticator.Writeable; + } + + /** + * Authenticate a user + * + * @param client Client information + * @param sess Server session + * @param alg Encryption algorithm + */ + public int authenticateUser(ClientInfo client, SrvSession sess, int alg) + { + + // Check if the user exists in the user list + + UserAccount userAcc = getUserDetails(client.getUserName()); + if (userAcc != null) + { + + // Validate the password + + boolean authSts = false; + + if (client.getPassword() != null) + { + + // Validate using the Unicode password + + authSts = validatePassword(userAcc.getPassword(), client.getPassword(), sess.getChallengeKey(), alg); + } + else if (client.hasANSIPassword()) + { + + // Validate using the ANSI password with the LanMan encryption + + authSts = validatePassword(userAcc.getPassword(), client.getANSIPassword(), sess.getChallengeKey(), + SrvAuthenticator.LANMAN); + } + + // Return the authentication status + + return authSts == true ? SrvAuthenticator.AUTH_ALLOW : SrvAuthenticator.AUTH_BADPASSWORD; + } + + // Check if this is an SMB/CIFS null session logon. + // + // The null session will only be allowed to connect to the IPC$ named pipe share. + + if (client.isNullSession() && sess instanceof SMBSrvSession) + return SrvAuthenticator.AUTH_ALLOW; + + // Unknown user + + return allowGuest() ? SrvAuthenticator.AUTH_GUEST : SrvAuthenticator.AUTH_DISALLOW; + } + + /** + * Generate a challenge key + * + * @param sess SrvSession + * @return byte[] + */ + public byte[] getChallengeKey(SrvSession sess) + { + + // Generate a new challenge key, pack the key and return + + byte[] key = new byte[8]; + + DataPacker.putIntelLong(m_random.nextLong(), key, 0); + return key; + } + + /** + * Search for the requried user account details in the defined user list + * + * @param user String + * @return UserAccount + */ + public UserAccount getUserDetails(String user) + { + + // Get the user account list from the configuration + + UserAccountList userList = m_config.getUserAccounts(); + if (userList == null || userList.numberOfUsers() == 0) + return null; + + // Search for the required user account record + + return userList.findUser(user); + } + + /** + * Initialize the authenticator + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initialize(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException + { + + // Save the server configuration so we can access the defined user list + + m_config = config; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/PasswordEncryptor.java b/source/java/org/alfresco/filesys/server/auth/PasswordEncryptor.java new file mode 100644 index 0000000000..cde91315a5 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/PasswordEncryptor.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +/** + * Password Encryptor Class + * + *

    Generates LanMan and NTLMv1 encrypted passwords from the plain text password and challenge key. + * + * @author GKSpencer + */ +public class PasswordEncryptor +{ + + // Encryption algorithm types + + public static final int LANMAN = 0; + public static final int NTLM1 = 1; + public static final int NTLM2 = 2; + public static final int MD4 = 3; + + // Encrpytion algorithm names + + private final static String[] _algNames = { "LanMan", "NTLMv1", "NTLMv2", "MD4" }; + + /** + * Default constructor + */ + public PasswordEncryptor() + { + } + + /** + * Check if the required algorithms are available + * + * @return boolean + */ + public static final boolean checkEncryptionAlgorithms() + { + + boolean algOK = false; + + try + { + + // Check if MD4 is available + + MessageDigest.getInstance("MD4"); + + // Check if DES is available + + Cipher.getInstance("DES"); + } + catch (NoSuchAlgorithmException ex) + { + } + catch (NoSuchPaddingException ex) + { + } + + // Return the encryption algorithm status + + return algOK; + } + + /** + * Encrypt the plain text password with the specified encryption key using the specified + * encryption algorithm. + * + * @param plainPwd Plaintext password string + * @param encryptKey byte[] Encryption key + * @param alg int Encryption algorithm + * @return byte[] Encrypted password + * @exception NoSuchAlgorithmException If a required encryption algorithm is not available + */ + public byte[] generateEncryptedPassword(String plainPwd, byte[] encryptKey, int alg) + throws NoSuchAlgorithmException + { + + // Get the password + + String pwd = plainPwd; + if (pwd == null) + pwd = ""; + + // Determine the encryption algorithm + + byte[] encPwd = null; + MessageDigest md4 = null; + int len = 0; + byte[] pwdBytes = null; + + switch (alg) + { + + // LanMan DES encryption + + case LANMAN: + encPwd = P24(pwd, encryptKey); + break; + + // NTLM v1 encryption + + case NTLM1: + + // Create the MD4 hash + + md4 = MessageDigest.getInstance("MD4"); + + try { + pwdBytes = pwd.getBytes("UnicodeLittleUnmarked"); + } + catch (UnsupportedEncodingException ex) { + } + + md4.update(pwdBytes); + byte[] p21 = new byte[21]; + System.arraycopy(md4.digest(), 0, p21, 0, 16); + + // Now use the LM encryption + + encPwd = P24(p21,encryptKey); + break; + + // NTLM v2 encryption + + case NTLM2: + break; + + // MD4 encryption + + case MD4: + + // Create the MD4 hash + + md4 = MessageDigest.getInstance("MD4"); + len = pwd.length(); + pwdBytes = new byte[len * 2]; + + for (int i = 0; i < len; i++) + { + char ch = pwd.charAt(i); + pwdBytes[i * 2] = (byte) ch; + pwdBytes[i * 2 + 1] = (byte) ((ch >> 8) & 0xFF); + } + + md4.update(pwdBytes); + encPwd = new byte[16]; + System.arraycopy(md4.digest(), 0, encPwd, 0, 16); + break; + } + + // Return the encrypted password + + return encPwd; + } + + /** + * P16 encryption + * + * @param pwd java.lang.String + * @param s8 byte[] + * @return byte[] + * @exception NoSuchAlgorithmException If a required encryption algorithm is not available + */ + public final byte[] P16(String pwd, byte[] s8) throws NoSuchAlgorithmException + { + + // Make a 14 byte string using the password string. Truncate the + // password or pad with nulls to 14 characters. + + StringBuffer p14str = new StringBuffer(); + p14str.append(pwd.toUpperCase()); + if (p14str.length() > 14) + p14str.setLength(14); + + while (p14str.length() < 14) + p14str.append((char) 0x00); + + // Convert the P14 string to an array of bytes. Allocate a 21 byte buffer as the result is usually passed + // through the P24() method + + byte[] p14 = p14str.toString().getBytes(); + byte[] p16 = new byte[21]; + + try + { + + // DES encrypt the password bytes using the challenge key + + Cipher des = Cipher.getInstance("DES"); + + // Set the encryption seed using the first 7 bytes of the password string. + // Generate the first 8 bytes of the return value. + + byte[] key = generateKey(p14, 0); + + SecretKeySpec chKey = new SecretKeySpec(key, 0, key.length, "DES"); + des.init(Cipher.ENCRYPT_MODE, chKey); + byte[] res = des.doFinal(s8); + System.arraycopy(res, 0, p16, 0, 8); + + // Encrypt the second block + + key = generateKey(p14, 7); + + chKey = new SecretKeySpec(key, 0, key.length, "DES"); + des.init(Cipher.ENCRYPT_MODE, chKey); + res = des.doFinal(s8); + System.arraycopy(res, 0, p16, 8, 8); + } + catch (NoSuchPaddingException ex) + { + p16 = null; + } + catch (IllegalBlockSizeException ex) + { + p16 = null; + } + catch (BadPaddingException ex) + { + p16 = null; + } + catch (InvalidKeyException ex) + { + p16 = null; + } + + // Return the 16 byte encrypted value + + return p16; + } + + /** + * P24 DES encryption + * + * @param pwd java.lang.String + * @param c8 byte[] + * @return byte[] + * @exception NoSuchAlgorithmException If a required encryption algorithm is not available + */ + private final byte[] P24(String pwd, byte[] c8) throws NoSuchAlgorithmException + { + + // Generate the 16 byte encrypted value using the password string and well + // known value. + + byte[] s8 = new String("KGS!@#$%").getBytes(); + byte[] p16 = P16(pwd, s8); + + // Generate the 24 byte encrypted value + + return P24(p16, c8); + } + + /** + * P24 DES encryption + * + * @param p21 Plain password or hashed password bytes + * @param ch Challenge bytes + * @return Encrypted password + * @exception NoSuchAlgorithmException If a required encryption algorithm is not available + */ + private final byte[] P24(byte[] p21, byte[] ch) throws NoSuchAlgorithmException + { + + byte[] enc = null; + + try + { + + // DES encrypt the password bytes using the challenge key + + Cipher des = Cipher.getInstance("DES"); + + // Allocate the output bytes + + enc = new byte[24]; + + // Encrypt the first block + + byte[] key = generateKey(p21, 0); + + SecretKeySpec chKey = new SecretKeySpec(key, 0, key.length, "DES"); + des.init(Cipher.ENCRYPT_MODE, chKey); + byte[] res = des.doFinal(ch); + System.arraycopy(res, 0, enc, 0, 8); + + // Encrypt the second block + + key = generateKey(p21, 7); + + chKey = new SecretKeySpec(key, 0, key.length, "DES"); + des.init(Cipher.ENCRYPT_MODE, chKey); + res = des.doFinal(ch); + System.arraycopy(res, 0, enc, 8, 8); + + // Encrypt the last block + + key = generateKey(p21, 14); + + chKey = new SecretKeySpec(key, 0, key.length, "DES"); + des.init(Cipher.ENCRYPT_MODE, chKey); + res = des.doFinal(ch); + System.arraycopy(res, 0, enc, 16, 8); + } + catch (NoSuchPaddingException ex) + { + ex.printStackTrace(); + enc = null; + } + catch (IllegalBlockSizeException ex) + { + ex.printStackTrace(); + enc = null; + } + catch (BadPaddingException ex) + { + ex.printStackTrace(); + enc = null; + } + catch (InvalidKeyException ex) + { + ex.printStackTrace(); + enc = null; + } + + // Return the encrypted password, or null if an error occurred + + return enc; + } + + /** + * Return the encryption algorithm as a string + * + * @param alg int + * @return String + */ + public static String getAlgorithmName(int alg) + { + if (alg >= 0 && alg < _algNames.length) + return _algNames[alg]; + return "Unknown"; + } + + /** + * Make a 7-byte string into a 64 bit/8 byte/longword key. + * + * @param byt byte[] + * @param off int + * @return byte[] + */ + private byte[] generateKey(byte[] byt, int off) + { + + // Allocate the key + + byte[] key = new byte[8]; + + // Make a key from the input string + + key[0] = (byte) (byt[off + 0] >> 1); + key[1] = (byte) (((byt[off + 0] & 0x01) << 6) | ((byt[off + 1] & 0xFF) >> 2)); + key[2] = (byte) (((byt[off + 1] & 0x03) << 5) | ((byt[off + 2] & 0xFF) >> 3)); + key[3] = (byte) (((byt[off + 2] & 0x07) << 4) | ((byt[off + 3] & 0xFF) >> 4)); + key[4] = (byte) (((byt[off + 3] & 0x0F) << 3) | ((byt[off + 4] & 0xFF) >> 5)); + key[5] = (byte) (((byt[off + 4] & 0x1F) << 2) | ((byt[off + 5] & 0xFF) >> 6)); + key[6] = (byte) (((byt[off + 5] & 0x3F) << 1) | ((byt[off + 6] & 0xFF) >> 7)); + key[7] = (byte) (byt[off + 6] & 0x7F); + + for (int i = 0; i < 8; i++) + { + key[i] = (byte) (key[i] << 1); + } + + return key; + } + + /** + * NTLM1 encryption of the MD4 hashed password + * + * @param p21 byte[] + * @param c8 byte[] + * @return byte[] + * @exception NoSuchAlgorithmException + */ + public final byte[] doNTLM1Encryption(byte[] p21, byte[] c8) + throws NoSuchAlgorithmException + { + return P24(p21, c8); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/SrvAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/SrvAuthenticator.java new file mode 100644 index 0000000000..bbb209a10e --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/SrvAuthenticator.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import java.security.NoSuchAlgorithmException; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + *

    + * An authenticator is used by the SMB server to authenticate users when in user level access mode + * and authenticate requests to connect to a share when in share level access. + */ +public abstract class SrvAuthenticator +{ + + // Encryption algorithm types + + public static final int LANMAN = PasswordEncryptor.LANMAN; + public static final int NTLM1 = PasswordEncryptor.NTLM1; + public static final int NTLM2 = PasswordEncryptor.NTLM2; + + // Authentication status values + + public static final int AUTH_ALLOW = 0; + public static final int AUTH_GUEST = 0x10000000; + public static final int AUTH_DISALLOW = -1; + public static final int AUTH_BADPASSWORD = -2; + public static final int AUTH_BADUSER = -3; + + // Share access permissions, returned by authenticateShareConnect() + + public static final int NoAccess = 0; + public static final int ReadOnly = 1; + public static final int Writeable = 2; + + // Server access mode + + public static final int SHARE_MODE = 0; + public static final int USER_MODE = 1; + + // Standard encrypted password length + + public static final int STANDARD_PASSWORD_LEN = 24; + + // Server access mode + + private int m_accessMode = SHARE_MODE; + + // Use encrypted password + + private boolean m_encryptPwd = false; + + // Password encryption algorithms + + private PasswordEncryptor m_encryptor = new PasswordEncryptor(); + + // Flag to enable/disable the guest account + + private boolean m_allowGuest; + + /** + * Authenticate a connection to a share. + * + * @param client User/client details from the tree connect request. + * @param share Shared device the client wants to connect to. + * @param pwd Share password. + * @param sess Server session. + * @return int Granted file permission level or disallow status if negative. See the + * FilePermission class. + */ + public abstract int authenticateShareConnect(ClientInfo client, SharedDevice share, String sharePwd, SrvSession sess); + + /** + * Authenticate a user. A user may be granted full access, guest access or no access. + * + * @param client User/client details from the session setup request. + * @param sess Server session + * @param alg Encryption algorithm + * @return int Access level or disallow status. + */ + public abstract int authenticateUser(ClientInfo client, SrvSession sess, int alg); + + /** + * Return the user account details for the specified user + * + * @param user String + * @return UserAccount + */ + public abstract UserAccount getUserDetails(String user); + + /** + * Authenticate a user using a plain text password. + * + * @param client User/client details from the session setup request. + * @param sess Server session + * @return int Access level or disallow status. + * @throws InvalidConfigurationException + */ + public final int authenticateUserPlainText(ClientInfo client, SrvSession sess) + { + + // Get a challenge key + + sess.setChallengeKey(getChallengeKey(sess)); + + if (sess.hasChallengeKey() == false) + return SrvAuthenticator.AUTH_DISALLOW; + + // Get the plain text password + + String textPwd = client.getPasswordAsString(); + if (textPwd == null) + textPwd = client.getANSIPasswordAsString(); + + // Encrypt the password + + byte[] encPwd = generateEncryptedPassword(textPwd, sess.getChallengeKey(), SrvAuthenticator.NTLM1); + client.setPassword(encPwd); + + // Authenticate the user + + return authenticateUser(client, sess, SrvAuthenticator.NTLM1); + } + + /** + * Initialize the authenticator + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initialize(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException + { + } + + /** + * Encrypt the plain text password with the specified encryption key using the specified + * encryption algorithm. + * + * @return byte[] + * @param plainPwd java.lang.String + * @param encryptKey byte[] + * @param alg int + */ + protected final byte[] generateEncryptedPassword(String plainPwd, byte[] encryptKey, int alg) + { + + // Use the password encryptor + + byte[] encPwd = null; + + try + { + + // Encrypt the password + + encPwd = m_encryptor.generateEncryptedPassword(plainPwd, encryptKey, alg); + } + catch (NoSuchAlgorithmException ex) + { + } + + // Return the encrypted password + + return encPwd; + } + + /** + * Return the access mode of the server, either SHARE_MODE or USER_MODE. + * + * @return int + */ + public final int getAccessMode() + { + return m_accessMode; + } + + /** + * Get a challenge encryption key, when encrypted passwords are enabled. + * + * @param sess SrvSession + * @return byte[] + */ + public abstract byte[] getChallengeKey(SrvSession sess); + + /** + * Determine if encrypted passwords should be used. + * + * @return boolean + */ + public final boolean hasEncryptPasswords() + { + return m_encryptPwd; + } + + /** + * Determine if guest access is allowed + * + * @return boolean + */ + public final boolean allowGuest() + { + return m_allowGuest; + } + + /** + * Set the access mode of the server. + * + * @param mode Either SHARE_MODE or USER_MODE. + */ + public final void setAccessMode(int mode) + { + m_accessMode = mode; + } + + /** + * Set/clear the encrypted passwords flag. + * + * @param encFlag Encrypt passwords if true, use plain text passwords if false. + */ + public final void setEncryptedPasswords(boolean encFlag) + { + m_encryptPwd = encFlag; + } + + /** + * Enable/disable the guest account + * + * @param ena Enable the guest account if true, only allow defined user accounts access if false + */ + public final void setAllowGuest(boolean ena) + { + m_allowGuest = ena; + } + + /** + * Close the authenticator, perform any cleanup + */ + public void closeAuthenticator() + { + // Override if cleanup required + } + + /** + * Validate a password by encrypting the plain text password using the specified encryption key + * and encryption algorithm. + * + * @return boolean + * @param plainPwd java.lang.String + * @param encryptedPwd java.lang.String + * @param encryptKey byte[] + * @param alg int + */ + protected final boolean validatePassword(String plainPwd, byte[] encryptedPwd, byte[] encryptKey, int alg) + { + + // Generate an encrypted version of the plain text password + + byte[] encPwd = generateEncryptedPassword(plainPwd != null ? plainPwd : "", encryptKey, alg); + + // Compare the generated password with the received password + + if (encPwd != null && encryptedPwd != null && encPwd.length == STANDARD_PASSWORD_LEN + && encryptedPwd.length == STANDARD_PASSWORD_LEN) + { + + // Compare the password arrays + + for (int i = 0; i < STANDARD_PASSWORD_LEN; i++) + if (encPwd[i] != encryptedPwd[i]) + return false; + + // Password is valid + + return true; + } + + // User or password is invalid + + return false; + } + + /** + * Convert the password string to a byte array + * + * @param pwd String + * @return byte[] + */ + + protected final byte[] convertPassword(String pwd) + { + + // Create a padded/truncated 14 character string + + StringBuffer p14str = new StringBuffer(); + p14str.append(pwd); + if (p14str.length() > 14) + p14str.setLength(14); + else + { + while (p14str.length() < 14) + p14str.append((char) 0x00); + } + + // Convert the P14 string to an array of bytes. Allocate the return 16 byte array. + + return p14str.toString().getBytes(); + } + + /** + * Return the password encryptor + * + * @return PasswordEncryptor + */ + protected final PasswordEncryptor getEncryptor() + { + return m_encryptor; + } + + /** + * Return the authentication status as a string + * + * @param sts int + * @return String + */ + protected final String getStatusAsString(int sts) + { + String str = null; + + switch ( sts) + { + case AUTH_ALLOW: + str = "Allow"; + break; + case AUTH_DISALLOW: + str = "Disallow"; + break; + case AUTH_GUEST: + str = "Guest"; + break; + case AUTH_BADPASSWORD: + str = "BadPassword"; + break; + case AUTH_BADUSER: + str = "BadUser"; + break; + } + + return str; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/UserAccount.java b/source/java/org/alfresco/filesys/server/auth/UserAccount.java new file mode 100644 index 0000000000..0171a68e2e --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/UserAccount.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import org.alfresco.filesys.util.StringList; + +/** + * User Account Class + *

    + * Holds the details of a user account on the server. + */ +public class UserAccount +{ + + // User name and password + + private String m_userName; + private String m_password; + + // Real user name and comment + + private String m_realName; + private String m_comment; + + // List of shares this user is allowed to use + + private StringList m_shares; + + // Administrator flag + + private boolean m_admin; + + // Home directory + + private String m_homeDir; + + /** + * Default constructor + */ + public UserAccount() + { + super(); + } + + /** + * Create a user with the specified name and password. + * + * @param user String + * @param pwd String + */ + public UserAccount(String user, String pwd) + { + setUserName(user); + setPassword(pwd); + } + + /** + * Add the specified share to the list of allowed shares for this user. + * + * @param shr java.lang.String + */ + public final void addShare(String shr) + { + if (m_shares == null) + m_shares = new StringList(); + m_shares.addString(shr); + } + + /** + * Determine if this user is allowed to access the specified share. + * + * @return boolean + * @param shr java.lang.String + */ + public final boolean allowsShare(String shr) + { + if (m_shares == null) + return true; + else if (m_shares.containsString(shr)) + return true; + return false; + } + + /** + * Check if the user has a home direectory configured + * + * @return boolean + */ + public final boolean hasHomeDirectory() + { + return m_homeDir != null ? true : false; + } + + /** + * Return the home directory for this user + * + * @return String + */ + public final String getHomeDirectory() + { + return m_homeDir; + } + + /** + * Return the password + * + * @return java.lang.String + */ + public final String getPassword() + { + return m_password; + } + + /** + * Return the user name. + * + * @return java.lang.String + */ + public final String getUserName() + { + return m_userName; + } + + /** + * Return the real user name + * + * @return String + */ + public final String getRealName() + { + return m_realName; + } + + /** + * Return the user comment + * + * @return String + */ + public final String getComment() + { + return m_comment; + } + + /** + * Check if the specified share is listed in the users allowed list. + * + * @return boolean + * @param shr java.lang.String + */ + public final boolean hasShare(String shr) + { + if (m_shares != null && m_shares.containsString(shr) == false) + return false; + return true; + } + + /** + * Detemrine if this account is restricted to using certain shares only. + * + * @return boolean + */ + public final boolean hasShareRestrictions() + { + return m_shares == null ? false : true; + } + + /** + * Return the list of shares + * + * @return StringList + */ + public final StringList getShareList() + { + return m_shares; + } + + /** + * Determine if this user in an administrator. + * + * @return boolean + */ + public final boolean isAdministrator() + { + return m_admin; + } + + /** + * Remove all shares from the list of restricted shares. + */ + public final void removeAllShares() + { + m_shares = null; + } + + /** + * Remove the specified share from the list of shares this user is allowed to access. + * + * @param shr java.lang.String + */ + public final void removeShare(String shr) + { + + // Check if the share list has been allocated + + if (m_shares != null) + { + + // Remove the share from the list + + m_shares.removeString(shr); + + // Check if the list is empty + + if (m_shares.numberOfStrings() == 0) + m_shares = null; + } + } + + /** + * Set the administrator flag. + * + * @param admin boolean + */ + public final void setAdministrator(boolean admin) + { + m_admin = admin; + } + + /** + * Set the user home directory + * + * @param home String + */ + public final void setHomeDirectory(String home) + { + m_homeDir = home; + } + + /** + * Set the password for this account. + * + * @param pwd java.lang.String + */ + public final void setPassword(String pwd) + { + m_password = pwd; + } + + /** + * Set the user name. + * + * @param user java.lang.String + */ + public final void setUserName(String user) + { + m_userName = user; + } + + /** + * Set the real user name + * + * @param name String + */ + public final void setRealName(String name) + { + m_realName = name; + } + + /** + * Set the comment + * + * @param comment String + */ + public final void setComment(String comment) + { + m_comment = comment; + } + + /** + * Return the user account as a string. + * + * @return java.lang.String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getUserName()); + str.append(":"); + str.append(getPassword()); + + if (isAdministrator()) + str.append("(ADMIN)"); + + str.append(",Real="); + + str.append(getRealName()); + str.append(",Comment="); + str.append(getComment()); + str.append(",Allow="); + + if (m_shares == null) + str.append(""); + else + str.append(m_shares); + str.append("]"); + + str.append(",Home="); + if (hasHomeDirectory()) + str.append(getHomeDirectory()); + else + str.append("None"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/UserAccountList.java b/source/java/org/alfresco/filesys/server/auth/UserAccountList.java new file mode 100644 index 0000000000..4d1d93b7b6 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/UserAccountList.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth; + +import java.util.Vector; + +/** + * User Account List Class + */ +public class UserAccountList +{ + + // User account list + + private Vector m_users; + + /** + * Create a user account list. + */ + public UserAccountList() + { + m_users = new Vector(); + } + + /** + * Add a user to the list of accounts. + * + * @param user UserAccount + */ + public final void addUser(UserAccount user) + { + + // Check if the user exists on the list + + removeUser(user); + m_users.add(user); + } + + /** + * Find the required user account details. + * + * @param user java.lang.String + * @return UserAccount + */ + public final UserAccount findUser(String user) + { + + // Search for the specified user account + + for (int i = 0; i < m_users.size(); i++) + { + UserAccount acc = m_users.get(i); + if (acc.getUserName().equalsIgnoreCase(user)) + return acc; + } + + // User not found + + return null; + } + + /** + * Determine if the specified user account exists in the list. + * + * @return boolean + * @param user java.lang.String + */ + public final boolean hasUser(String user) + { + + // Search for the specified user account + + for (int i = 0; i < m_users.size(); i++) + { + UserAccount acc = m_users.get(i); + if (acc.getUserName().compareTo(user) == 0) + return true; + } + + // User not found + + return false; + } + + /** + * Return the specified user account details + * + * @param idx int + * @return UserAccount + */ + public final UserAccount getUserAt(int idx) + { + if (idx >= m_users.size()) + return null; + return m_users.get(idx); + } + + /** + * Return the number of defined user accounts. + * + * @return int + */ + public final int numberOfUsers() + { + return m_users.size(); + } + + /** + * Remove all user accounts from the list. + */ + public final void removeAllUsers() + { + m_users.removeAllElements(); + } + + /** + * Remvoe the specified user account from the list. + * + * @param userAcc UserAccount + */ + public final void removeUser(UserAccount userAcc) + { + + // Search for the specified user account + + for (int i = 0; i < m_users.size(); i++) + { + UserAccount acc = m_users.get(i); + if (acc.getUserName().compareTo(userAcc.getUserName()) == 0) + { + m_users.remove(i); + return; + } + } + } + + /** + * Remvoe the specified user account from the list. + * + * @param user java.lang.String + */ + public final void removeUser(String user) + { + + // Search for the specified user account + + for (int i = 0; i < m_users.size(); i++) + { + UserAccount acc = m_users.get(i); + if (acc.getUserName().compareTo(user) == 0) + { + m_users.remove(i); + return; + } + } + } + + /** + * Return the user account list as a string. + * + * @return java.lang.String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(m_users.size()); + str.append(":"); + + for (int i = 0; i < m_users.size(); i++) + { + UserAccount acc = m_users.get(i); + str.append(acc.getUserName()); + str.append(","); + } + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/acl/ACLParseException.java b/source/java/org/alfresco/filesys/server/auth/acl/ACLParseException.java new file mode 100644 index 0000000000..13b7fad05b --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/ACLParseException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +/** + * Access Control Parse Exception Class + */ +public class ACLParseException extends Exception +{ + private static final long serialVersionUID = 3978983284405776688L; + + /** + * Default constructor. + */ + public ACLParseException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public ACLParseException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/AccessControl.java b/source/java/org/alfresco/filesys/server/auth/acl/AccessControl.java new file mode 100644 index 0000000000..e95b7be7b5 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/AccessControl.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import java.util.StringTokenizer; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + * Access Control Base Class + *

    + * Controls access to a shared filesystem. + */ +public abstract class AccessControl +{ + + // Access control type/status + + public final static int NoAccess = 0; + public final static int ReadOnly = 1; + public final static int ReadWrite = 2; + + public final static int MaxLevel = 2; + + // Default access status, indicates that the access conrol did not apply + + public final static int Default = -1; + + // Access type strings + + private final static String[] _accessType = { "None", "Read", "Write" }; + + // Access control name and type + + private String m_name; + private String m_type; + + // Access type + + private int m_access; + + /** + * Class constructor + * + * @param name String + * @param type String + * @param access int + */ + protected AccessControl(String name, String type, int access) + { + setName(name); + setType(type); + + m_access = access; + } + + /** + * Return the access control name + * + * @return String + */ + public final String getName() + { + return m_name; + } + + /** + * Return the access control type + * + * @return String + */ + public final String getType() + { + return m_type; + } + + /** + * Return the access control check type + * + * @return int + */ + public final int getAccess() + { + return m_access; + } + + /** + * Return the access control check type as a string + * + * @return String + */ + public final String getAccessString() + { + return _accessType[m_access]; + } + + /** + * Check if the specified session has access to the shared device. + * + * @param sess SrvSession + * @param share SharedDevice + * @param mgr AccessControlManager + * @return int + */ + public abstract int allowsAccess(SrvSession sess, SharedDevice share, AccessControlManager mgr); + + /** + * Return the index of a value from a list of valid values, or 01 if not valid + * + * @param val String + * @param list String[] + * @param caseSensitive boolean + * @return int + */ + protected final static int indexFromList(String val, String[] valid, boolean caseSensitive) + { + + // Check if the value is valid + + if (val == null || val.length() == 0) + return -1; + + // Search for the matching value in the valid list + + for (int i = 0; i < valid.length; i++) + { + + // Check the current value in the valid list + + if (caseSensitive) + { + if (valid[i].equals(val)) + return i; + } + else if (valid[i].equalsIgnoreCase(val)) + return i; + } + + // Value does not match any of the valid values + + return -1; + } + + /** + * Create a list of valid strings from a comma delimeted list + * + * @param str String + * @return String[] + */ + protected final static String[] listFromString(String str) + { + + // Check if the string is valid + + if (str == null || str.length() == 0) + return null; + + // Split the comma delimeted string into an array of strings + + StringTokenizer token = new StringTokenizer(str, ","); + int numStrs = token.countTokens(); + if (numStrs == 0) + return null; + + String[] list = new String[numStrs]; + + // Parse the string into a list of strings + + int i = 0; + + while (token.hasMoreTokens()) + list[i++] = token.nextToken(); + + // Return the string list + + return list; + } + + /** + * Set the access control type + * + * @param typ String + */ + protected final void setType(String typ) + { + m_type = typ; + } + + /** + * Set the access control name + * + * @param name String + */ + protected final void setName(String name) + { + m_name = name; + } + + /** + * Return the access control type as a string + * + * @param access int + * @return String + */ + public static final String asAccessString(int access) + { + if (access == Default) + return "Default"; + return _accessType[access]; + } + + /** + * Return the access control as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getType()); + str.append(":"); + str.append(getName()); + str.append(","); + str.append(getAccessString()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/AccessControlFactory.java b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlFactory.java new file mode 100644 index 0000000000..6d35df66bd --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import java.util.Hashtable; + +import org.alfresco.config.ConfigElement; + +/** + * Access Control Factoy Class + *

    + * The AccessControlFactory class holds a table of available AccessControlParsers that are used to + * generate AccessControl instances. + *

    + * An AccessControlParser has an associated unique type name that is used to call the appropriate + * parser. + */ +public class AccessControlFactory +{ + + // Access control parsers + + private Hashtable m_parsers; + + /** + * Class constructor + */ + public AccessControlFactory() + { + m_parsers = new Hashtable(); + } + + /** + * Create an access control using the specified parameters + * + * @param type String + * @param params ConfigElement + * @return AccessControl + * @exception ACLParseException + * @exception InvalidACLTypeException + */ + public final AccessControl createAccessControl(String type, ConfigElement params) throws ACLParseException, + InvalidACLTypeException + { + + // Find the access control parser + + AccessControlParser parser = m_parsers.get(type); + if (parser == null) + throw new InvalidACLTypeException(type); + + // Parse the parameters and create a new AccessControl instance + + return parser.createAccessControl(params); + } + + /** + * Add a parser to the list of available parsers + * + * @param parser AccessControlParser + */ + public final void addParser(AccessControlParser parser) + { + m_parsers.put(parser.getType(), parser); + } + + /** + * Remove a parser from the available parser list + * + * @param type String + * @return AccessControlParser + */ + public final AccessControlParser removeParser(String type) + { + return (AccessControlParser) m_parsers.remove(type); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/AccessControlList.java b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlList.java new file mode 100644 index 0000000000..0a32d02ed7 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlList.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import java.util.Vector; + +/** + * Access Control List Class + *

    + * Contains a list of access controls for a shared filesystem. + */ +public class AccessControlList +{ + + // Access control list + + private Vector m_list; + + // Default access level applied when rules return a default status + + private int m_defaultAccess = AccessControl.ReadWrite; + + /** + * Create an access control list. + */ + public AccessControlList() + { + m_list = new Vector(); + } + + /** + * Get the default access level + * + * @return int + */ + public final int getDefaultAccessLevel() + { + return m_defaultAccess; + } + + /** + * Set the default access level + * + * @param level int + * @exception InvalidACLTypeException If the access level is invalid + */ + public final void setDefaultAccessLevel(int level) throws InvalidACLTypeException + { + + // Check the default access level + + if (level < AccessControl.NoAccess || level > AccessControl.MaxLevel) + throw new InvalidACLTypeException(); + + // Set the default access level for the access control list + + m_defaultAccess = level; + } + + /** + * Add an access control to the list + * + * @param accCtrl AccessControl + */ + public final void addControl(AccessControl accCtrl) + { + + // Add the access control to the list + + m_list.add(accCtrl); + } + + /** + * Return the specified access control + * + * @param idx int + * @return AccessControl + */ + public final AccessControl getControlAt(int idx) + { + if (idx < 0 || idx >= m_list.size()) + return null; + return m_list.get(idx); + } + + /** + * Return the number of access controls in the list + * + * @return int + */ + public final int numberOfControls() + { + return m_list.size(); + } + + /** + * Remove all access controls from the list + */ + public final void removeAllControls() + { + m_list.removeAllElements(); + } + + /** + * Remove the specified access control from the list. + * + * @param idx int + * @return AccessControl + */ + public final AccessControl removeControl(int idx) + { + if (idx < 0 || idx >= m_list.size()) + return null; + return m_list.remove(idx); + } + + /** + * Return the access control list as a string. + * + * @return java.lang.String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(m_list.size()); + str.append(":"); + + str.append(":"); + str.append(AccessControl.asAccessString(getDefaultAccessLevel())); + str.append(":"); + + for (int i = 0; i < m_list.size(); i++) + { + AccessControl ctrl = m_list.get(i); + str.append(ctrl.toString()); + str.append(","); + } + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/AccessControlManager.java b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlManager.java new file mode 100644 index 0000000000..9440d06a67 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlManager.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; + +/** + * Access Control Manager Interface + *

    + * Used to control access to shared filesystems. + * + * @author Gary K. Spencer + */ +public interface AccessControlManager +{ + + /** + * Initialize the access control manager + * + * @param config ServerConfiguration + * @param params ConfigElement + */ + public void initialize(ServerConfiguration config, ConfigElement params); + + /** + * Check access to the shared filesystem for the specified session + * + * @param sess SrvSession + * @param share SharedDevice + * @return int + */ + public int checkAccessControl(SrvSession sess, SharedDevice share); + + /** + * Filter a shared device list to remove shares that are not visible or the session does not + * have access to. + * + * @param sess SrvSession + * @param shares SharedDeviceList + * @return SharedDeviceList + */ + public SharedDeviceList filterShareList(SrvSession sess, SharedDeviceList shares); + + /** + * Create an access control + * + * @param type String + * @param params ConfigElement + * @return AccessControl + * @exception ACLParseException + * @exception InvalidACLTypeException + */ + public AccessControl createAccessControl(String type, ConfigElement params) throws ACLParseException, + InvalidACLTypeException; + + /** + * Add an access control parser to the list of available access control types. + * + * @param parser AccessControlParser + */ + public void addAccessControlType(AccessControlParser parser); +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/AccessControlParser.java b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlParser.java new file mode 100644 index 0000000000..39330dbb8e --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/AccessControlParser.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.config.ConfigElement; + +/** + * Access Control Parser Class + *

    + * Creates an AccessControl instance by parsing a set of name/value parameters. + */ +public abstract class AccessControlParser +{ + + // Constants + // + // Standard parameter names + + public final static String ParameterAccess = "access"; + + // Access control type names + + private final static String[] _accessTypes = { "None", "Read", "Write" }; + + /** + * Return the access control type name that uniquely identifies this type of access control. + * + * @return String + */ + public abstract String getType(); + + /** + * Create an AccessControl instance by parsing the set of name/value parameters + * + * @param params ConfigElement + * @return AccessControl + * @exception ACLParseException + */ + public abstract AccessControl createAccessControl(ConfigElement params) throws ACLParseException; + + /** + * Find the access parameter and parse the value + * + * @param params ConfigElement + * @return int + * @exception ACLParseException + */ + protected final int parseAccessType(ConfigElement params) throws ACLParseException + { + + // Check if the parameter list is valid + + if (params == null) + throw new ACLParseException("Empty parameter list"); + + // Find the access type parameter + + String accessType = params.getAttribute(ParameterAccess); + + if (accessType == null || accessType.length() == 0) + throw new ACLParseException("Required parameter 'access' missing"); + + // Parse the access type value + + return parseAccessTypeString(accessType); + } + + /** + * Parse the access level type and validate + * + * @param accessType String + * @return int + * @exception ACLParseException + */ + public static final int parseAccessTypeString(String accessType) throws ACLParseException + { + + // Check if the access type is valid + + if (accessType == null || accessType.length() == 0) + throw new ACLParseException("Empty access type string"); + + // Parse the access type value + + int access = -1; + + for (int i = 0; i < _accessTypes.length; i++) + { + + // Check if the access type matches the current type + + if (accessType.equalsIgnoreCase(_accessTypes[i])) + access = i; + } + + // Check if we found a valid access type + + if (access == -1) + throw new ACLParseException("Invalid access type, " + accessType); + + // Return the access type + + return access; + } + + /** + * Return the parser details as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getType()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/DefaultAccessControlManager.java b/source/java/org/alfresco/filesys/server/auth/acl/DefaultAccessControlManager.java new file mode 100644 index 0000000000..c4ab73984e --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/DefaultAccessControlManager.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import java.util.Enumeration; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Default Access Control Manager Class + *

    + * Default access control manager implementation. + * + * @author Gary K. Spencer + */ +public class DefaultAccessControlManager implements AccessControlManager +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Access control factory + + private AccessControlFactory m_factory; + + // Debug enable flag + + private boolean m_debug; + + /** + * Class constructor + */ + public DefaultAccessControlManager() + { + + // Create the access control factory + + m_factory = new AccessControlFactory(); + } + + /** + * Check if the session has access to the shared device. + * + * @param sess SrvSession + * @param share SharedDevice + * @return int + */ + public int checkAccessControl(SrvSession sess, SharedDevice share) + { + + // Check if the shared device has any access control configured + + if (share.hasAccessControls() == false) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Check access control for " + share.getName() + ", no ACLs"); + + // Allow full access to the share + + return AccessControl.ReadWrite; + } + + // Process the access control list + + AccessControlList acls = share.getAccessControls(); + int access = AccessControl.Default; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Check access control for " + share.getName() + ", ACLs=" + acls.numberOfControls()); + + for (int i = 0; i < acls.numberOfControls(); i++) + { + + // Get the current access control and run + + AccessControl acl = acls.getControlAt(i); + int curAccess = acl.allowsAccess(sess, share, this); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug(" Check access ACL=" + acl + ", access=" + AccessControl.asAccessString(curAccess)); + + // Update the allowed access + + if (curAccess != AccessControl.Default) + access = curAccess; + } + + // Check if the default access level is still selected, if so then get the default level + // from the + // access control list + + if (access == AccessControl.Default) + { + + // Use the default access level + + access = acls.getDefaultAccessLevel(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Access defaulted=" + AccessControl.asAccessString(access) + ", share=" + share); + } + else if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Access allowed=" + AccessControl.asAccessString(access) + ", share=" + share); + + // Return the access type + + return access; + } + + /** + * Filter the list of shared devices to return a list that contains only the shares that are + * visible or accessible by the session. + * + * @param sess SrvSession + * @param shares SharedDeviceList + * @return SharedDeviceList + */ + public SharedDeviceList filterShareList(SrvSession sess, SharedDeviceList shares) + { + + // Check if the share list is valid or empty + + if (shares == null || shares.numberOfShares() == 0) + return shares; + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Filter share list for " + sess + ", shares=" + shares); + + // For each share in the list check the access, remove any shares that the session does not + // have access to. + + SharedDeviceList filterList = new SharedDeviceList(); + Enumeration enm = shares.enumerateShares(); + + while (enm.hasMoreElements()) + { + + // Get the current share + + SharedDevice share = enm.nextElement(); + + // Check if the share has any access controls + + if (share.hasAccessControls()) + { + + // Check if the session has access to this share + + int access = checkAccessControl(sess, share); + if (access != AccessControl.NoAccess) + filterList.addShare(share); + } + else + { + + // Add the share to the filtered list + + filterList.addShare(share); + } + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Filtered share list " + filterList); + + // Return the filtered share list + + return filterList; + } + + /** + * Initialize the access control manager + * + * @param config ServerConfiguration + * @param params ConfigElement + */ + public void initialize(ServerConfiguration config, ConfigElement params) + { + + // Check if debug output is enabled + + if (params != null && params.getChild("debug") != null) + setDebug(true); + + // Add the default access control types + + addAccessControlType(new UserAccessControlParser()); + addAccessControlType(new ProtocolAccessControlParser()); + addAccessControlType(new DomainAccessControlParser()); + addAccessControlType(new IpAddressAccessControlParser()); + } + + /** + * Create an access control. + * + * @param type String + * @param params ConfigElement + * @return AccessControl + * @throws ACLParseException + * @throws InvalidACLTypeException + */ + public AccessControl createAccessControl(String type, ConfigElement params) throws ACLParseException, + InvalidACLTypeException + { + + // Use the access control factory to create the access control instance + + return m_factory.createAccessControl(type, params); + } + + /** + * Add an access control parser to the list of available access control types. + * + * @param parser AccessControlParser + */ + public void addAccessControlType(AccessControlParser parser) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("AccessControlManager Add rule type " + parser.getType()); + + // Add the new access control type to the factory + + m_factory.addParser(parser); + } + + /** + * Determine if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Enable/disable debug output + * + * @param dbg boolean + */ + public final void setDebug(boolean dbg) + { + m_debug = dbg; + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControl.java b/source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControl.java new file mode 100644 index 0000000000..6d69bf8600 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControl.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + * Domain Name Access Control Class + *

    + * Allow/disallow access based on the SMB/CIFS session callers domain name. + */ +public class DomainAccessControl extends AccessControl +{ + + /** + * Class constructor + * + * @param domainName String + * @param type String + * @param access int + */ + protected DomainAccessControl(String domainName, String type, int access) + { + super(domainName, type, access); + } + + /** + * Check if the domain name matches the access control domain name and return the allowed + * access. + * + * @param sess SrvSession + * @param share SharedDevice + * @param mgr AccessControlManager + * @return int + */ + public int allowsAccess(SrvSession sess, SharedDevice share, AccessControlManager mgr) + { + + // Check if the session has client information + + if (sess.hasClientInformation() == false + || sess instanceof org.alfresco.filesys.smb.server.SMBSrvSession == false) + return Default; + + // Check if the domain name matches the access control name + + ClientInfo cInfo = sess.getClientInformation(); + + if (cInfo.getDomain() != null && cInfo.getDomain().equalsIgnoreCase(getName())) + return getAccess(); + return Default; + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControlParser.java b/source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControlParser.java new file mode 100644 index 0000000000..88997753b5 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/DomainAccessControlParser.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.config.ConfigElement; + +/** + * Domain Name Access Control Parser Class + */ +public class DomainAccessControlParser extends AccessControlParser +{ + + /** + * Default constructor + */ + public DomainAccessControlParser() + { + } + + /** + * Return the parser type + * + * @return String + */ + public String getType() + { + return "domain"; + } + + /** + * Validate the parameters and create a user access control + * + * @param params ConfigElement + * @return AccessControl + * @throws ACLParseException + */ + public AccessControl createAccessControl(ConfigElement params) throws ACLParseException + { + + // Get the access type + + int access = parseAccessType(params); + + // Get the domain name to check for + + String domainName = params.getAttribute("name"); + if (domainName == null || domainName.length() == 0) + throw new ACLParseException("Domain name not specified"); + + // Create the domain access control + + return new DomainAccessControl(domainName, getType(), access); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/InvalidACLTypeException.java b/source/java/org/alfresco/filesys/server/auth/acl/InvalidACLTypeException.java new file mode 100644 index 0000000000..aa13a601ef --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/InvalidACLTypeException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +/** + * Invalid ACL Type Exception Class + */ +public class InvalidACLTypeException extends Exception +{ + private static final long serialVersionUID = 3257844398418310708L; + + /** + * Default constructor. + */ + public InvalidACLTypeException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public InvalidACLTypeException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControl.java b/source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControl.java new file mode 100644 index 0000000000..2499e1094c --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControl.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import java.net.InetAddress; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.util.IPAddress; + +/** + * Ip Address Access Control Class + *

    + * Allow/disallow access by checking for a particular TCP/IP address or checking that the address is + * within a specified subnet. + */ +public class IpAddressAccessControl extends AccessControl +{ + + // Subnet and network mask if the address specifies the subnet + + private String m_subnet; + private String m_netMask; + + /** + * Class constructor + * + * @param address String + * @param mask String + * @param type String + * @param access int + */ + protected IpAddressAccessControl(String address, String mask, String type, int access) + { + super(address, type, access); + + // Save the subnet and network mask, if specified + + m_subnet = address; + m_netMask = mask; + + // Change the rule name if a network mask has been specified + + if (m_netMask != null) + setName(m_subnet + "/" + m_netMask); + } + + /** + * Check if the TCP/IP address matches the specifed address or is within the subnet. + * + * @param sess SrvSession + * @param share SharedDevice + * @param mgr AccessControlManager + * @return int + */ + public int allowsAccess(SrvSession sess, SharedDevice share, AccessControlManager mgr) + { + + // Check if the remote address is set for the session + + InetAddress remoteAddr = sess.getRemoteAddress(); + + if (remoteAddr == null) + return Default; + + // Get the remote address as a numeric IP address string + + String ipAddr = remoteAddr.getHostAddress(); + + // Check if the access control is a single TCP/IP address check + + int sts = Default; + + if (m_netMask == null) + { + + // Check if the TCP/IP address matches the check address + + if (IPAddress.parseNumericAddress(ipAddr) == IPAddress.parseNumericAddress(getName())) + sts = getAccess(); + } + else + { + + // Check if the address is within the subnet range + + if (IPAddress.isInSubnet(ipAddr, m_subnet, m_netMask) == true) + sts = getAccess(); + } + + // Return the access status + + return sts; + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControlParser.java b/source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControlParser.java new file mode 100644 index 0000000000..1440cbcf53 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/IpAddressAccessControlParser.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.util.IPAddress; + +/** + * Ip Address Access Control Parser Class + */ +public class IpAddressAccessControlParser extends AccessControlParser +{ + + /** + * Default constructor + */ + public IpAddressAccessControlParser() + { + } + + /** + * Return the parser type + * + * @return String + */ + public String getType() + { + return "address"; + } + + /** + * Validate the parameters and create an address access control + * + * @param params ConfigElement + * @return AccessControl + * @throws ACLParseException + */ + public AccessControl createAccessControl(ConfigElement params) throws ACLParseException + { + + // Get the access type + + int access = parseAccessType(params); + + // Check if the single IP address format has been specified + + String ipAddr = params.getAttribute("ip"); + if (ipAddr != null) + { + + // Validate the parameters + + if (ipAddr.length() == 0 || IPAddress.isNumericAddress(ipAddr) == false) + throw new ACLParseException("Invalid IP address, " + ipAddr); + + if (params.getAttributeCount() != 2) + throw new ACLParseException("Invalid parameter(s) specified for address"); + + // Create a single TCP/IP address access control rule + + return new IpAddressAccessControl(ipAddr, null, getType(), access); + } + + // Check if a subnet address and mask have been specified + + String subnet = params.getAttribute("subnet"); + if (subnet != null) + { + + // Get the network mask parameter + + String netmask = params.getAttribute("mask"); + + // Validate the parameters + + if (subnet.length() == 0 || netmask == null || netmask.length() == 0) + throw new ACLParseException("Invalid subnet/mask parameter"); + + if (IPAddress.isNumericAddress(subnet) == false) + throw new ACLParseException("Invalid subnet parameter, " + subnet); + + if (IPAddress.isNumericAddress(netmask) == false) + throw new ACLParseException("Invalid mask parameter, " + netmask); + + // Create a subnet address access control rule + + return new IpAddressAccessControl(subnet, netmask, getType(), access); + } + + // Invalid parameters + + throw new ACLParseException("Unknown address parameter(s)"); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControl.java b/source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControl.java new file mode 100644 index 0000000000..e73dbe881f --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControl.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import java.util.StringTokenizer; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + * Protocol Access Control Class + *

    + * Allow/disallow access to a share based on the protocol type. + */ +public class ProtocolAccessControl extends AccessControl +{ + + // Available protocol type names + + private static final String[] _protoTypes = { "SMB", "CIFS", "NFS", "FTP" }; + + // Parsed list of protocol types + + private String[] m_checkList; + + /** + * Class constructor + * + * @param protList String + * @param type String + * @param access int + */ + protected ProtocolAccessControl(String protList, String type, int access) + { + super(protList, type, access); + + // Parse the protocol list + + m_checkList = listFromString(protList); + } + + /** + * Check if the protocol matches the access control protocol list and return the allowed access. + * + * @param sess SrvSession + * @param share SharedDevice + * @param mgr AccessControlManager + * @return int + */ + public int allowsAccess(SrvSession sess, SharedDevice share, AccessControlManager mgr) + { + + // Determine the session protocol type + + String sessProto = null; + String sessName = sess.getClass().getName(); + + if (sessName.endsWith(".SMBSrvSession")) + sessProto = "CIFS"; + else if (sessName.endsWith(".FTPSrvSession")) + sessProto = "FTP"; + else if (sessName.endsWith(".NFSSrvSession")) + sessProto = "NFS"; + + // Check if the session protocol type is in the protocols to be checked + + if (sessProto != null && indexFromList(sessProto, m_checkList, false) != -1) + return getAccess(); + return Default; + } + + /** + * Validate the protocol list + * + * @param protList String + * @return boolean + */ + public static final boolean validateProtocolList(String protList) + { + + // Check if the protocol list string is valid + + if (protList == null || protList.length() == 0) + return false; + + // Split the protocol list and validate each protocol name + + StringTokenizer tokens = new StringTokenizer(protList, ","); + + while (tokens.hasMoreTokens()) + { + + // Get the current protocol name and validate + + String name = tokens.nextToken().toUpperCase(); + if (indexFromList(name, _protoTypes, false) == -1) + return false; + } + + // Protocol list is valid + + return true; + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControlParser.java b/source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControlParser.java new file mode 100644 index 0000000000..55ae19957d --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/ProtocolAccessControlParser.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.config.ConfigElement; + +/** + * Protocol Access Control Parser Class + */ +public class ProtocolAccessControlParser extends AccessControlParser +{ + /** + * Default constructor + */ + public ProtocolAccessControlParser() + { + } + + /** + * Return the parser type + * + * @return String + */ + public String getType() + { + return "protocol"; + } + + /** + * Validate the parameters and create a user access control + * + * @param params ConfigElement + * @return AccessControl + * @throws ACLParseException + */ + public AccessControl createAccessControl(ConfigElement params) throws ACLParseException + { + + // Get the access type + + int access = parseAccessType(params); + + // Get the list of protocols to check for + + String protos = params.getAttribute("type"); + if (protos == null || protos.length() == 0) + throw new ACLParseException("Protocol type not specified"); + + // Validate the protocol list + + if (ProtocolAccessControl.validateProtocolList(protos) == false) + throw new ACLParseException("Invalid protocol type"); + + // Create the protocol access control + + return new ProtocolAccessControl(protos, getType(), access); + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/UserAccessControl.java b/source/java/org/alfresco/filesys/server/auth/acl/UserAccessControl.java new file mode 100644 index 0000000000..e6fae5dcc8 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/UserAccessControl.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + * User Access Control Class + *

    + * Allow/disallow access to a shared device by checking the user name. + */ +public class UserAccessControl extends AccessControl +{ + /** + * Class constructor + * + * @param userName String + * @param type String + * @param access int + */ + protected UserAccessControl(String userName, String type, int access) + { + super(userName, type, access); + } + + /** + * Check if the user name matches the access control user name and return the allowed access. + * + * @param sess SrvSession + * @param share SharedDevice + * @param mgr AccessControlManager + * @return int + */ + public int allowsAccess(SrvSession sess, SharedDevice share, AccessControlManager mgr) + { + + // Check if the session has client information + + if (sess.hasClientInformation() == false) + return Default; + + // Check if the user name matches the access control name + + ClientInfo cInfo = sess.getClientInformation(); + + if (cInfo.getUserName() != null && cInfo.getUserName().equalsIgnoreCase(getName())) + return getAccess(); + return Default; + } +} diff --git a/source/java/org/alfresco/filesys/server/auth/acl/UserAccessControlParser.java b/source/java/org/alfresco/filesys/server/auth/acl/UserAccessControlParser.java new file mode 100644 index 0000000000..b5e66fc3ac --- /dev/null +++ b/source/java/org/alfresco/filesys/server/auth/acl/UserAccessControlParser.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.auth.acl; + +import org.alfresco.config.ConfigElement; + +/** + * User Access Control Parser Class + */ +public class UserAccessControlParser extends AccessControlParser +{ + /** + * Default constructor + */ + public UserAccessControlParser() + { + } + + /** + * Return the parser type + * + * @return String + */ + public String getType() + { + return "user"; + } + + /** + * Validate the parameters and create a user access control + * + * @param params ConfigElement + * @return AccessControl + * @throws ACLParseException + */ + public AccessControl createAccessControl(ConfigElement params) throws ACLParseException + { + + // Get the access type + + int access = parseAccessType(params); + + // Get the user name to check for + + String userName = params.getAttribute("name"); + if (userName == null || userName.length() == 0) + throw new ACLParseException("User name not specified"); + + // Create the user access control + + return new UserAccessControl(userName, getType(), access); + } +} diff --git a/source/java/org/alfresco/filesys/server/config/IncompleteConfigurationException.java b/source/java/org/alfresco/filesys/server/config/IncompleteConfigurationException.java new file mode 100644 index 0000000000..f7ff454bc4 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/config/IncompleteConfigurationException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.config; + +/** + *

    + * Indicates that the server configuration is incomplete, and the server cannot be started. + *

    + * The server name, domain name and network broadcast mask are the minimum parameters that must be + * specified for a server configuration. + */ +public class IncompleteConfigurationException extends Exception +{ + private static final long serialVersionUID = 3617577102334244400L; + + /** + * IncompleteConfigurationException constructor. + */ + public IncompleteConfigurationException() + { + super(); + } + + /** + * IncompleteConfigurationException constructor. + * + * @param s java.lang.String + */ + public IncompleteConfigurationException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/config/InvalidConfigurationException.java b/source/java/org/alfresco/filesys/server/config/InvalidConfigurationException.java new file mode 100644 index 0000000000..ac48960ad1 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/config/InvalidConfigurationException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.config; + +/** + *

    + * Indicates that one or more parameters in the server configuration are not valid. + */ +public class InvalidConfigurationException extends Exception +{ + private static final long serialVersionUID = 3257568390900887607L; + + /** + * InvalidConfigurationException constructor. + * + * @param s java.lang.String + */ + public InvalidConfigurationException(String s) + { + super(s); + } + + /** + * InvalidConfigurationException constructor. + * + * @param s java.lang.String + * @param ex Exception + */ + public InvalidConfigurationException(String s, Throwable ex) + { + super(s, ex); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java b/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java new file mode 100644 index 0000000000..6973ed51dd --- /dev/null +++ b/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java @@ -0,0 +1,2930 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.config; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.Provider; +import java.security.Security; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.List; +import java.util.StringTokenizer; +import java.util.TimeZone; + +import net.sf.acegisecurity.AuthenticationManager; + +import org.alfresco.config.Config; +import org.alfresco.config.ConfigElement; +import org.alfresco.config.ConfigLookupContext; +import org.alfresco.config.source.ClassPathConfigSource; +import org.alfresco.config.xml.XMLConfigService; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.ftp.FTPPath; +import org.alfresco.filesys.ftp.InvalidPathException; +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.NetBIOSNameList; +import org.alfresco.filesys.netbios.NetBIOSSession; +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.netbios.win32.Win32NetBIOS; +import org.alfresco.filesys.server.NetworkServer; +import org.alfresco.filesys.server.NetworkServerList; +import org.alfresco.filesys.server.auth.LocalAuthenticator; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.auth.UserAccount; +import org.alfresco.filesys.server.auth.UserAccountList; +import org.alfresco.filesys.server.auth.acl.ACLParseException; +import org.alfresco.filesys.server.auth.acl.AccessControl; +import org.alfresco.filesys.server.auth.acl.AccessControlList; +import org.alfresco.filesys.server.auth.acl.AccessControlManager; +import org.alfresco.filesys.server.auth.acl.AccessControlParser; +import org.alfresco.filesys.server.auth.acl.DefaultAccessControlManager; +import org.alfresco.filesys.server.auth.acl.InvalidACLTypeException; +import org.alfresco.filesys.server.core.DeviceContext; +import org.alfresco.filesys.server.core.DeviceContextException; +import org.alfresco.filesys.server.core.ShareMapper; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.alfresco.filesys.server.filesys.DefaultShareMapper; +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.DiskSharedDevice; +import org.alfresco.filesys.server.filesys.HomeShareMapper; +import org.alfresco.filesys.smb.Dialect; +import org.alfresco.filesys.smb.DialectSelector; +import org.alfresco.filesys.smb.ServerType; +import org.alfresco.filesys.util.IPAddress; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

    + * Provides the configuration parameters for the network file servers. + * + * @author Gary K. Spencer + */ +public class ServerConfiguration +{ + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Filesystem configuration constants + + private static final String ConfigArea = "file-servers"; + private static final String ConfigCIFS = "CIFS Server"; + private static final String ConfigFTP = "FTP Server"; + private static final String ConfigFilesystems = "Filesystems"; + private static final String ConfigSecurity = "Filesystem Security"; + + // Server configuration bean name + + public static final String SERVER_CONFIGURATION = "fileServerConfiguration"; + + // SMB/CIFS session debug type strings + // + // Must match the bit mask order + + private static final String m_sessDbgStr[] = { "NETBIOS", "STATE", "NEGOTIATE", "TREE", "SEARCH", "INFO", "FILE", + "FILEIO", "TRANSACT", "ECHO", "ERROR", "IPC", "LOCK", "PKTTYPE", "DCERPC", "STATECACHE", "NOTIFY", + "STREAMS", "SOCKET" }; + + // FTP server debug type strings + + private static final String m_ftpDebugStr[] = { "STATE", "SEARCH", "INFO", "FILE", "FILEIO", "ERROR", "PKTTYPE", + "TIMING", "DATAPORT", "DIRECTORY" }; + + // Default FTP server port + + private static final int DefaultFTPServerPort = 21; + + // Default FTP anonymous account name + + private static final String DefaultFTPAnonymousAccount = "anonymous"; + + // Platform types + + public enum PlatformType + { + Unknown, WINDOWS, LINUX, SOLARIS, MACOSX + }; + + // Token name to substitute current server name into the CIFS server name + private static final String TokenLocalName = "${localname}"; + + // Acegi authentication manager + private AuthenticationManager acegiAuthMgr; + + // Path to configuration file + private String configLocation; + + /** the device to connect use */ + private DiskInterface diskInterface; + + // Runtime platform type + + private PlatformType m_platform = PlatformType.Unknown; + + // Main server enable flags, to enable SMB, FTP and/or NFS server components + + private boolean m_smbEnable = true; + private boolean m_ftpEnable = true; + + // Server name + private String m_name; + + // Server type, used by the host announcer + private int m_srvType = ServerType.WorkStation + ServerType.Server + ServerType.NTServer; + + // Active server list + private NetworkServerList m_serverList; + + // Server comment + private String m_comment; + + // Server domain + private String m_domain; + + // Network broadcast mask string + private String m_broadcast; + + // Announce the server to network neighborhood, announcement interval in + // minutes + private boolean m_announce; + + private int m_announceInterval; + + // Default SMB dialects to enable + private DialectSelector m_dialects; + + // List of shared devices + private SharedDeviceList m_shareList; + + // Authenticator, used to authenticate users and share connections. + private SrvAuthenticator m_authenticator; + + // Share mapper + private ShareMapper m_shareMapper; + + // Access control manager + private AccessControlManager m_aclManager; + + // Global access control list, applied to all shares that do not have access + // controls + private AccessControlList m_globalACLs; + + // SMB server, NetBIOS name server and host announcer debug enable + private boolean m_srvDebug = false; + + private boolean m_nbDebug = false; + + private boolean m_announceDebug = false; + + // Default session debugging setting + private int m_sessDebug; + + // Flags to indicate if NetBIOS, native TCP/IP SMB and/or Win32 NetBIOS + // should be enabled + private boolean m_netBIOSEnable = true; + + private boolean m_tcpSMBEnable = false; + + private boolean m_win32NBEnable = false; + + // Address to bind the SMB server to, if null all local addresses are used + private InetAddress m_smbBindAddress; + + // Address to bind the NetBIOS name server to, if null all addresses are + // used + private InetAddress m_nbBindAddress; + + // WINS servers + private InetAddress m_winsPrimary; + private InetAddress m_winsSecondary; + + // User account list + private UserAccountList m_userList; + + // Enable/disable Macintosh extension SMBs + private boolean m_macExtensions; + + // -------------------------------------------------------------------------------- + // Win32 NetBIOS configuration + // + // Server name to register under Win32 NetBIOS, if not set the main server + // name is used + private String m_win32NBName; + + // LANA to be used for Win32 NetBIOS, if not specified the first available + // is used + private int m_win32NBLANA = -1; + + // Send out host announcements via the Win32 NetBIOS interface + private boolean m_win32NBAnnounce = false; + private int m_win32NBAnnounceInterval; + + // Use Winsock NetBIOS interface if true, else use the Netbios() API interface + + private boolean m_win32NBUseWinsock = true; + + // -------------------------------------------------------------------------------- + // FTP specific configuration parameters + // + // Bind address and FTP server port. + + private InetAddress m_ftpBindAddress; + private int m_ftpPort = DefaultFTPServerPort; + + // Allow anonymous FTP access and anonymous FTP account name + + private boolean m_ftpAllowAnonymous; + private String m_ftpAnonymousAccount; + + // FTP root path, if not specified defaults to listing all shares as the root + + private String m_ftpRootPath; + + // FTP server debug flags + + private int m_ftpDebug; + + // -------------------------------------------------------------------------------- + // Global server configuration + // + // Timezone name and offset from UTC in minutes + private String m_timeZone; + private int m_tzOffset; + + // JCE provider class name + private String m_jceProviderClass; + + // Local server name and domain/workgroup name + + private String m_localName; + private String m_localDomain; + + /** flag indicating successful initialisation */ + private boolean initialised; + + // Main authentication service, public API + + private AuthenticationService authenticationService; + + // Authentication component, for internal functions + + private AuthenticationComponent m_authComponent; + + // Various services + + private NodeService m_nodeService; + private PersonService m_personService; + private TransactionService m_transactionService; + + /** + * Class constructor + * + * @param authMgr AuthenticationManager + * @param authenticationService AuthenticationService + * @param authenticationComponent AuthenticationComponent + * @param nodeService NodeService + * @param personServce PersonService + * @param transactionService TransactionService + * @param configPath String + * @param diskInterface DiskInterface + */ + public ServerConfiguration(AuthenticationManager authMgr, AuthenticationService authenticationService, + AuthenticationComponent authComponent, NodeService nodeService, PersonService personService, + TransactionService transactionService, String configPath, DiskInterface diskInterface) + { + // Save details + + this.diskInterface = diskInterface; + this.acegiAuthMgr = authMgr; + this.authenticationService = authenticationService; + this.configLocation = configPath; + + m_authComponent = authComponent; + + m_nodeService = nodeService; + m_personService = personService; + m_transactionService = transactionService; + + // Allocate the shared device list + + m_shareList = new SharedDeviceList(); + + // Allocate the SMB dialect selector, and initialize using the default + // list of dialects + + m_dialects = new DialectSelector(); + + m_dialects.AddDialect(Dialect.DOSLanMan1); + m_dialects.AddDialect(Dialect.DOSLanMan2); + m_dialects.AddDialect(Dialect.LanMan1); + m_dialects.AddDialect(Dialect.LanMan2); + m_dialects.AddDialect(Dialect.LanMan2_1); + m_dialects.AddDialect(Dialect.NT); + + // Use the local authenticator, that allows locally defined users to connect to the + // server + + setAuthenticator(new LocalAuthenticator(), null, true); + + // Use the default share mapper + + m_shareMapper = new DefaultShareMapper(); + + try + { + m_shareMapper.initializeMapper(this, null); + } + catch (InvalidConfigurationException ex) + { + throw new AlfrescoRuntimeException("Failed to initialise share mapper", ex); + } + + // Set the default access control manager + + m_aclManager = new DefaultAccessControlManager(); + m_aclManager.initialize(this, null); + + // Use the default timezone + + try + { + setTimeZone(TimeZone.getDefault().getID()); + } + catch (Exception ex) + { + throw new AlfrescoRuntimeException("Failed to set timezone", ex); + } + + // Allocate the active server list + + m_serverList = new NetworkServerList(); + } + + /** + * @return Returns true if the configuration was fully initialised + */ + public boolean isInitialised() + { + return initialised; + } + + /** + * Initialize the configuration using the configuration service + */ + public void init() + { + initialised = false; + + // Create the configuration source + + ClassPathConfigSource classPathConfigSource = new ClassPathConfigSource(configLocation); + XMLConfigService xmlConfigService = new XMLConfigService(classPathConfigSource); + xmlConfigService.init(); + + // Create the configuration context + + ConfigLookupContext configCtx = new ConfigLookupContext(ConfigArea); + + // Set the platform type + + determinePlatformType(); + + try + { + + // Process the CIFS server configuration + + Config config = xmlConfigService.getConfig(ConfigCIFS, configCtx); + processCIFSServerConfig(config); + + // Process the FTP server configuration + + config = xmlConfigService.getConfig(ConfigFTP, configCtx); + processFTPServerConfig(config); + + // Process the security configuration + + config = xmlConfigService.getConfig(ConfigSecurity, configCtx); + processSecurityConfig(config); + + // Process the filesystems configuration + + config = xmlConfigService.getConfig(ConfigFilesystems, configCtx); + processFilesystemsConfig(config); + + // Successful initialisation + + initialised = true; + } + catch (UnsatisfiedLinkError ex) + { + // Error accessing the Win32NetBIOS DLL code + + logger.error("Error accessing Win32 NetBIOS, check DLL is on the path"); + + // Disable the CIFS server + + setNetBIOSSMB(false); + setTcpipSMB(false); + setWin32NetBIOS(false); + + setSMBServerEnabled(false); + } + catch (Throwable ex) + { + // Configuration error + + logger.error("CIFS server configuration error, " + ex.getMessage(), ex); + + // Disable the CIFS server + + setNetBIOSSMB(false); + setTcpipSMB(false); + setWin32NetBIOS(false); + + setSMBServerEnabled(false); + } + } + + /** + * Determine the platform type + */ + private final void determinePlatformType() + { + // Get the operating system type + + String osName = System.getProperty("os.name"); + + if (osName.startsWith("Windows")) + m_platform = PlatformType.WINDOWS; + else if (osName.equalsIgnoreCase("Linux")) + m_platform = PlatformType.LINUX; + else if (osName.startsWith("Mac OS X")) + m_platform = PlatformType.MACOSX; + else if (osName.startsWith("Solaris")) + m_platform = PlatformType.SOLARIS; + } + + /** + * Return the platform type + * + * @return PlatformType + */ + public final PlatformType getPlatformType() + { + return m_platform; + } + + /** + * Process the CIFS server configuration + * + * @param config Config + */ + private final void processCIFSServerConfig(Config config) + { + // Get the network broadcast address + // + // Note: We need to set this first as the call to getLocalDomainName() may use a NetBIOS + // name lookup, so the broadcast mask must be set before then. + + ConfigElement elem = config.getConfigElement("broadcast"); + if (elem != null) + { + + // Check if the broadcast mask is a valid numeric IP address + + if (IPAddress.isNumericAddress(elem.getValue()) == false) + throw new AlfrescoRuntimeException("Invalid broadcast mask, must be n.n.n.n format"); + + // Set the network broadcast mask + + setBroadcastMask(elem.getValue()); + } + + // Get the host configuration + + elem = config.getConfigElement("host"); + if (elem == null) + throw new AlfrescoRuntimeException("CIFS server host settings not specified"); + + String hostName = elem.getAttribute("name"); + if (hostName == null || hostName.length() == 0) + throw new AlfrescoRuntimeException("Host name not specified or invalid"); + + // Check if the host name contains the local name token + + int pos = hostName.indexOf(TokenLocalName); + if (pos != -1) + { + + // Get the local server name + + String srvName = getLocalServerName(true); + + // Rebuild the host name substituting the token with the local server name + + StringBuilder hostStr = new StringBuilder(); + + hostStr.append(hostName.substring(0, pos)); + hostStr.append(srvName); + + pos += TokenLocalName.length(); + if (pos < hostName.length()) + hostStr.append(hostName.substring(pos)); + + hostName = hostStr.toString(); + + // Make sure the CIFS server name does not match the local server name + + if (hostName.equals(srvName)) + throw new AlfrescoRuntimeException("CIFS server name must be unique"); + } + + // Set the CIFS server name + + setServerName(hostName.toUpperCase()); + + // Get the domain/workgroup name + + String domain = elem.getAttribute("domain"); + if (domain != null && domain.length() > 0) + { + // Set the domain/workgroup name + + setDomainName(domain.toUpperCase()); + } + else + { + // Get the local domain/workgroup name + + setDomainName(getLocalDomainName()); + } + + // Check for a server comment + + elem = config.getConfigElement("comment"); + if (elem != null) + setComment(elem.getValue()); + + // Check for a bind address + + elem = config.getConfigElement("bindto"); + if (elem != null) + { + + // Validate the bind address + + String bindText = elem.getValue(); + + try + { + + // Check the bind address + + InetAddress bindAddr = InetAddress.getByName(bindText); + + // Set the bind address for the server + + setSMBBindAddress(bindAddr); + } + catch (UnknownHostException ex) + { + throw new AlfrescoRuntimeException("Invalid CIFS server bind address"); + } + } + + // Check if the host announcer should be enabled + + elem = config.getConfigElement("hostAnnounce"); + if (elem != null) + { + + // Check for an announcement interval + + String interval = elem.getAttribute("interval"); + if (interval != null && interval.length() > 0) + { + try + { + setHostAnnounceInterval(Integer.parseInt(interval)); + } + catch (NumberFormatException ex) + { + throw new AlfrescoRuntimeException("Invalid host announcement interval"); + } + } + + // Check if the domain name has been set, this is required if the + // host announcer is enabled + + if (getDomainName() == null) + throw new AlfrescoRuntimeException("Domain name must be specified if host announcement is enabled"); + + // Enable host announcement + + setHostAnnouncer(true); + } + + // Check if NetBIOS SMB is enabled + + elem = config.getConfigElement("netBIOSSMB"); + if (elem != null) + { + // Check if NetBIOS over TCP/IP is enabled for the current platform + + String platformsStr = elem.getAttribute("platforms"); + boolean platformOK = false; + + if (platformsStr != null) + { + // Parse the list of platforms that NetBIOS over TCP/IP is to be enabled for and + // check if the current platform is included + + EnumSet enabledPlatforms = parsePlatformString(platformsStr); + if (enabledPlatforms.contains(getPlatformType())) + platformOK = true; + } + else + { + // No restriction on platforms + + platformOK = true; + } + + // Check if the broadcast mask has been specified + + if (getBroadcastMask() == null) + throw new AlfrescoRuntimeException("Network broadcast mask not specified"); + + // Enable the NetBIOS SMB support, if enabled for this platform + + setNetBIOSSMB(platformOK); + + // Check for a bind address + + String bindto = elem.getAttribute("bindto"); + if (bindto != null && bindto.length() > 0) + { + + // Validate the bind address + + try + { + + // Check the bind address + + InetAddress bindAddr = InetAddress.getByName(bindto); + + // Set the bind address for the NetBIOS name server + + setNetBIOSBindAddress(bindAddr); + } + catch (UnknownHostException ex) + { + throw new AlfrescoRuntimeException("Invalid NetBIOS bind address"); + } + } + else if (hasSMBBindAddress()) + { + + // Use the SMB bind address for the NetBIOS name server + + setNetBIOSBindAddress(getSMBBindAddress()); + } + } + else + { + + // Disable NetBIOS SMB support + + setNetBIOSSMB(false); + } + + // Check if TCP/IP SMB is enabled + + elem = config.getConfigElement("tcpipSMB"); + if (elem != null) + { + + // Check if native SMB is enabled for the current platform + + String platformsStr = elem.getAttribute("platforms"); + boolean platformOK = false; + + if (platformsStr != null) + { + // Parse the list of platforms that native SMB is to be enabled for and + // check if the current platform is included + + EnumSet enabledPlatforms = parsePlatformString(platformsStr); + if (enabledPlatforms.contains(getPlatformType())) + platformOK = true; + } + else + { + // No restriction on platforms + + platformOK = true; + } + + // Enable the TCP/IP SMB support, if enabled for this platform + + setTcpipSMB(platformOK); + } + else + { + + // Disable TCP/IP SMB support + + setTcpipSMB(false); + } + + // Check if Win32 NetBIOS is enabled + + elem = config.getConfigElement("Win32NetBIOS"); + if (elem != null) + { + + // Check if the Win32 NetBIOS server name has been specified + + String win32Name = elem.getAttribute("name"); + if (win32Name != null && win32Name.length() > 0) + { + + // Validate the name + + if (win32Name.length() > 16) + throw new AlfrescoRuntimeException("Invalid Win32 NetBIOS name, " + win32Name); + + // Set the Win32 NetBIOS file server name + + setWin32NetBIOSName(win32Name); + } + + // Check if the Win32 NetBIOS LANA has been specified + + String lanaStr = elem.getAttribute("lana"); + if (lanaStr != null && lanaStr.length() > 0) + { + + // Validate the LANA number + + int lana = -1; + + try + { + lana = Integer.parseInt(lanaStr); + } + catch (NumberFormatException ex) + { + throw new AlfrescoRuntimeException("Invalid win32 NetBIOS LANA specified"); + } + + // LANA should be in the range 0-255 + + if (lana < 0 || lana > 255) + throw new AlfrescoRuntimeException("Invalid Win32 NetBIOS LANA number, " + lana); + + // Set the LANA number + + setWin32LANA(lana); + } + + // Check if the native NetBIOS interface has been specified, either 'winsock' or 'netbios' + + String nativeAPI = elem.getAttribute("api"); + if ( nativeAPI != null && nativeAPI.length() > 0) + { + // Validate the API type + + boolean useWinsock = true; + + if ( nativeAPI.equalsIgnoreCase("netbios")) + useWinsock = false; + else if ( nativeAPI.equalsIgnoreCase("winsock") == false) + throw new AlfrescoRuntimeException("Invalid NetBIOS API type, spefify 'winsock' or 'netbios'"); + + // Set the NetBIOS API to use + + setWin32WinsockNetBIOS( useWinsock); + } + + // Check if the current operating system is supported by the Win32 + // NetBIOS handler + + String osName = System.getProperty("os.name"); + if (osName.startsWith("Windows") + && (osName.endsWith("95") == false && osName.endsWith("98") == false && osName.endsWith("ME") == false)) + { + + // Call the Win32NetBIOS native code to make sure it is initialized + + if ( Win32NetBIOS.LanaEnumerate() != null) + { + // Enable Win32 NetBIOS + + setWin32NetBIOS(true); + } + else + { + logger.warn("No NetBIOS LANAs available"); + } + } + else + { + + // Win32 NetBIOS not supported on the current operating system + + setWin32NetBIOS(false); + } + } + else + { + + // Disable Win32 NetBIOS + + setWin32NetBIOS(false); + } + + // Check if the host announcer should be enabled + + elem = config.getConfigElement("Win32Announce"); + if (elem != null) + { + + // Check for an announcement interval + + String interval = elem.getAttribute("interval"); + if (interval != null && interval.length() > 0) + { + try + { + setWin32HostAnnounceInterval(Integer.parseInt(interval)); + } + catch (NumberFormatException ex) + { + throw new AlfrescoRuntimeException("Invalid host announcement interval"); + } + } + + // Check if the domain name has been set, this is required if the + // host announcer is enabled + + if (getDomainName() == null) + throw new AlfrescoRuntimeException("Domain name must be specified if host announcement is enabled"); + + // Enable Win32 NetBIOS host announcement + + setWin32HostAnnouncer(true); + } + + // Check if NetBIOS and/or TCP/IP SMB have been enabled + + if (hasNetBIOSSMB() == false && hasTcpipSMB() == false && hasWin32NetBIOS() == false) + throw new AlfrescoRuntimeException("NetBIOS SMB, TCP/IP SMB or Win32 NetBIOS must be enabled"); + + // Check if WINS servers are configured + + elem = config.getConfigElement("WINS"); + + if (elem != null) + { + + // Get the primary WINS server + + ConfigElement priWinsElem = elem.getChild("primary"); + + if (priWinsElem == null || priWinsElem.getValue().length() == 0) + throw new AlfrescoRuntimeException("No primary WINS server configured"); + + // Validate the WINS server address + + InetAddress primaryWINS = null; + + try + { + primaryWINS = InetAddress.getByName(priWinsElem.getValue()); + } + catch (UnknownHostException ex) + { + throw new AlfrescoRuntimeException("Invalid primary WINS server address, " + priWinsElem.getValue()); + } + + // Check if a secondary WINS server has been specified + + ConfigElement secWinsElem = elem.getChild("secondary"); + InetAddress secondaryWINS = null; + + if (secWinsElem != null) + { + + // Validate the secondary WINS server address + + try + { + secondaryWINS = InetAddress.getByName(secWinsElem.getValue()); + } + catch (UnknownHostException ex) + { + throw new AlfrescoRuntimeException("Invalid secondary WINS server address, " + + secWinsElem.getValue()); + } + } + + // Set the WINS server address(es) + + setPrimaryWINSServer(primaryWINS); + if (secondaryWINS != null) + setSecondaryWINSServer(secondaryWINS); + + // Pass the setting to the NetBIOS session class + + NetBIOSSession.setWINSServer(primaryWINS); + } + + // Check if WINS is configured, if we are running on Windows and socket based NetBIOS is enabled + + else if (hasNetBIOSSMB() && getPlatformType() == PlatformType.WINDOWS) + { + // Get the WINS server list + + String winsServers = Win32NetBIOS.getWINSServerList(); + + if (winsServers != null) + { + // Use the first WINS server address for now + + StringTokenizer tokens = new StringTokenizer(winsServers, ","); + String addr = tokens.nextToken(); + + try + { + // Convert to a network address and check if the WINS server is accessible + + InetAddress winsAddr = InetAddress.getByName(addr); + + Socket winsSocket = new Socket(); + InetSocketAddress sockAddr = new InetSocketAddress( winsAddr, RFCNetBIOSProtocol.NAME_PORT); + + winsSocket.connect(sockAddr, 3000); + winsSocket.close(); + + // Set the primary WINS server address + + setPrimaryWINSServer(winsAddr); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("Configuring to use WINS server " + addr); + } + catch (UnknownHostException ex) + { + throw new AlfrescoRuntimeException("Invalid auto WINS server address, " + addr); + } + catch (IOException ex) + { + if ( logger.isDebugEnabled()) + logger.debug("Failed to connect to auto WINS server " + addr); + } + } + } + + // Check if session debug is enabled + + elem = config.getConfigElement("sessionDebug"); + if (elem != null) + { + + // Check for session debug flags + + String flags = elem.getAttribute("flags"); + int sessDbg = 0; + + if (flags != null) + { + + // Parse the flags + + flags = flags.toUpperCase(); + StringTokenizer token = new StringTokenizer(flags, ","); + + while (token.hasMoreTokens()) + { + + // Get the current debug flag token + + String dbg = token.nextToken().trim(); + + // Find the debug flag name + + int idx = 0; + + while (idx < m_sessDbgStr.length && m_sessDbgStr[idx].equalsIgnoreCase(dbg) == false) + idx++; + + if (idx > m_sessDbgStr.length) + throw new AlfrescoRuntimeException("Invalid session debug flag, " + dbg); + + // Set the debug flag + + sessDbg += 1 << idx; + } + } + + // Set the session debug flags + + setSessionDebugFlags(sessDbg); + } + } + + /** + * Process the FTP server configuration + * + * @param config Config + */ + private final void processFTPServerConfig(Config config) + { + // If the configuration section is not valid then FTP is disabled + + if ( config == null) + { + setFTPServerEnabled(false); + return; + } + + // Check for a bind address + + ConfigElement elem = config.getConfigElement("bindto"); + if ( elem != null) { + + // Validate the bind address + + String bindText = elem.getValue(); + + try { + + // Check the bind address + + InetAddress bindAddr = InetAddress.getByName(bindText); + + // Set the bind address for the FTP server + + setFTPBindAddress(bindAddr); + } + catch (UnknownHostException ex) { + throw new AlfrescoRuntimeException("Invalid FTP bindto address, " + elem.getValue()); + } + } + + // Check for an FTP server port + + elem = config.getConfigElement("port"); + if ( elem != null) { + try { + setFTPPort(Integer.parseInt(elem.getValue())); + if ( getFTPPort() <= 0 || getFTPPort() >= 65535) + throw new AlfrescoRuntimeException("FTP server port out of valid range"); + } + catch (NumberFormatException ex) { + throw new AlfrescoRuntimeException("Invalid FTP server port"); + } + } + else { + + // Use the default FTP port + + setFTPPort(DefaultFTPServerPort); + } + + // Check if anonymous login is allowed + + elem = config.getConfigElement("allowAnonymous"); + if ( elem != null) { + + // Enable anonymous login to the FTP server + + setAllowAnonymousFTP(true); + + // Check if an anonymous account has been specified + + String anonAcc = elem.getAttribute("user"); + if ( anonAcc != null && anonAcc.length() > 0) { + + // Set the anonymous account name + + setAnonymousFTPAccount(anonAcc); + + // Check if the anonymous account name is valid + + if ( getAnonymousFTPAccount() == null || getAnonymousFTPAccount().length() == 0) + throw new AlfrescoRuntimeException("Anonymous FTP account invalid"); + } + else { + + // Use the default anonymous account name + + setAnonymousFTPAccount(DefaultFTPAnonymousAccount); + } + } + else { + + // Disable anonymous logins + + setAllowAnonymousFTP(false); + } + + // Check if a root path has been specified + + elem = config.getConfigElement("rootDirectory"); + if ( elem != null) { + + // Get the root path + + String rootPath = elem.getValue(); + + // Validate the root path + + try { + + // Parse the path + + FTPPath ftpPath = new FTPPath(rootPath); + + // Set the root path + + setFTPRootPath(rootPath); + } + catch (InvalidPathException ex) { + throw new AlfrescoRuntimeException("Invalid FTP root directory, " + rootPath); + } + } + + // Check if FTP debug is enabled + + elem = config.getConfigElement("debug"); + if (elem != null) { + + // Check for FTP debug flags + + String flags = elem.getAttribute("flags"); + int ftpDbg = 0; + + if ( flags != null) { + + // Parse the flags + + flags = flags.toUpperCase(); + StringTokenizer token = new StringTokenizer(flags,","); + + while ( token.hasMoreTokens()) { + + // Get the current debug flag token + + String dbg = token.nextToken().trim(); + + // Find the debug flag name + + int idx = 0; + + while ( idx < m_ftpDebugStr.length && m_ftpDebugStr[idx].equalsIgnoreCase(dbg) == false) + idx++; + + if ( idx >= m_ftpDebugStr.length) + throw new AlfrescoRuntimeException("Invalid FTP debug flag, " + dbg); + + // Set the debug flag + + ftpDbg += 1 << idx; + } + } + + // Set the FTP debug flags + + setFTPDebug(ftpDbg); + } + } + + /** + * Process the filesystems configuration + * + * @param config Config + */ + private final void processFilesystemsConfig(Config config) + { + // Check for the home folder filesystem + + ConfigElement homeElem = config.getConfigElement("homeFolder"); + + if ( homeElem != null) + { + try + { + // Create the home folder share mapper + + HomeShareMapper shareMapper = new HomeShareMapper(); + shareMapper.initializeMapper( this, homeElem); + + // Use the home folder share mapper + + m_shareMapper = shareMapper; + + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Using home folder share mapper"); + } + catch (InvalidConfigurationException ex) + { + throw new AlfrescoRuntimeException("Failed to initialize home folder share mapper", ex); + } + } + + // Get the filesystem configuration elements + + List filesysElems = config.getConfigElementList("filesystem"); + + if (filesysElems != null) + { + + // Add the filesystems + + for (int i = 0; i < filesysElems.size(); i++) + { + + // Get the current filesystem configuration + + ConfigElement elem = filesysElems.get(i); + String filesysName = elem.getAttribute("name"); + + try + { + // Create a new filesystem driver instance and create a context for + // the new filesystem + DiskInterface filesysDriver = this.diskInterface; + DiskDeviceContext filesysContext = (DiskDeviceContext) filesysDriver.createContext(elem); + + // Check if an access control list has been specified + + AccessControlList acls = null; + ConfigElement aclElem = elem.getChild("accessControl"); + + if (aclElem != null) + { + + // Parse the access control list + + acls = processAccessControlList(aclElem); + } + else if (hasGlobalAccessControls()) + { + + // Use the global access control list for this disk share + + acls = getGlobalAccessControls(); + } + + // Check if change notifications are disabled + + boolean changeNotify = elem.getChild("disableChangeNotification") == null ? true : false; + + // Create the shared filesystem + + DiskSharedDevice filesys = new DiskSharedDevice(filesysName, filesysDriver, filesysContext); + + // Add any access controls to the share + + filesys.setAccessControlList(acls); + + // Enable/disable change notification for this device + + filesysContext.enableChangeHandler(changeNotify); + + // Start the filesystem + + filesysContext.startFilesystem(filesys); + + // Create the shared device and add to the list of available + // shared filesystems + + addShare(filesys); + } + catch (DeviceContextException ex) + { + throw new AlfrescoRuntimeException("Error creating filesystem " + filesysName, ex); + } + } + } + } + + /** + * Process the security configuration + * + * @param config Config + */ + private final void processSecurityConfig(Config config) + { + + // Check if global access controls have been specified + + ConfigElement globalACLs = config.getConfigElement("globalAccessControl"); + if (globalACLs != null) + { + + // Parse the access control list + + AccessControlList acls = processAccessControlList(globalACLs); + if (acls != null) + setGlobalAccessControls(acls); + } + + // Check if a JCE provider class has been specified + + ConfigElement jceElem = config.getConfigElement("JCEProvider"); + if (jceElem != null) + { + + // Set the JCE provider + + setJCEProvider(jceElem.getValue()); + } + else + { + // Use the default Cryptix JCE provider + + setJCEProvider("cryptix.jce.provider.CryptixCrypto"); + } + + // Check if an authenticator has been specified + + ConfigElement authElem = config.getConfigElement("authenticator"); + if (authElem != null) + { + + // Get the authenticator type, should be either 'local' or 'passthru' + + String authType = authElem.getAttribute("type"); + if (authType == null) + throw new AlfrescoRuntimeException("Authenticator type not specified"); + + // Set the authenticator class to use + + SrvAuthenticator auth = null; + if (authType.equalsIgnoreCase("local")) + auth = new LocalAuthenticator(); + else if (authType.equalsIgnoreCase("passthru")) + { + // Load the passthru authenticator dynamically + + auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.passthru.PassthruAuthenticator"); + if ( auth == null) + throw new AlfrescoRuntimeException("Failed to load passthru authenticator"); + } + else if (authType.equalsIgnoreCase("acegi")) + { + // Load the Acegi authenticator dynamically + + auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.passthru.AcegiPassthruAuthenticator"); + if ( auth == null) + throw new AlfrescoRuntimeException("Failed to load Acegi passthru authenticator"); + } + else if (authType.equalsIgnoreCase("alfresco")) + { + // Load the Alfresco authenticator dynamically + + auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.ntlm.AlfrescoAuthenticator"); + if ( auth == null) + auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.AlfrescoAuthenticator"); + + if ( auth == null) + throw new AlfrescoRuntimeException("Failed to load Alfresco authenticator"); + } + else + throw new AlfrescoRuntimeException("Invalid authenticator type, " + authType); + + // Get the allow guest setting + + boolean allowGuest = authElem.getChild("allowGuest") != null ? true : false; + + // Initialize and set the authenticator class + + setAuthenticator(auth, authElem, allowGuest); + } + + // Add the users + + ConfigElement usersElem = config.getConfigElement("users"); + if (usersElem != null) + { + + // Get the list of user elements + + List userElemList = usersElem.getChildren(); + + for (int i = 0; i < userElemList.size(); i++) + { + + // Get the current user element + + ConfigElement curUserElem = userElemList.get(i); + + if (curUserElem.getName().equals("localuser")) + { + processUser(curUserElem); + } + } + } + + } + + /** + * Process an access control sub-section and return the access control list + * + * @param aclsElem ConfigElement + */ + private final AccessControlList processAccessControlList(ConfigElement aclsElem) + { + + // Check if there is an access control manager configured + + if (getAccessControlManager() == null) + throw new AlfrescoRuntimeException("No access control manager configured"); + + // Create the access control list + + AccessControlList acls = new AccessControlList(); + + // Check if there is a default access level for the ACL group + + String attrib = aclsElem.getAttribute("default"); + + if (attrib != null && attrib.length() > 0) + { + + // Get the access level and validate + + try + { + + // Parse the access level name + + int access = AccessControlParser.parseAccessTypeString(attrib); + + // Set the default access level for the access control list + + acls.setDefaultAccessLevel(access); + } + catch (InvalidACLTypeException ex) + { + throw new AlfrescoRuntimeException("Default access level error", ex); + } + catch (ACLParseException ex) + { + throw new AlfrescoRuntimeException("Default access level error", ex); + } + } + + // Parse each access control element + + List aclElemList = aclsElem.getChildren(); + + if (aclElemList != null && aclElemList.size() > 0) + { + + // Create the access controls + + for (int i = 0; i < aclsElem.getChildCount(); i++) + { + + // Get the current ACL element + + ConfigElement curAclElem = aclElemList.get(i); + + try + { + // Create the access control and add to the list + + acls.addControl(getAccessControlManager().createAccessControl(curAclElem.getName(), curAclElem)); + } + catch (InvalidACLTypeException ex) + { + throw new AlfrescoRuntimeException("Invalid access control type - " + curAclElem.getName()); + } + catch (ACLParseException ex) + { + throw new AlfrescoRuntimeException("Access control parse error (" + curAclElem.getName() + ")", ex); + } + } + } + + // Check if there are no access control rules but the default access level is set to 'None', + // this is not allowed as the share would not be accessible or visible. + + if (acls.getDefaultAccessLevel() == AccessControl.NoAccess && acls.numberOfControls() == 0) + throw new AlfrescoRuntimeException("Empty access control list and default access 'None' not allowed"); + + // Return the access control list + + return acls; + } + + /** + * Add a user account + * + * @param user ConfigElement + */ + private final void processUser(ConfigElement user) + { + + // Get the username + + String attr = user.getAttribute("name"); + if (attr == null || attr.length() == 0) + throw new AlfrescoRuntimeException("User name not specified, or zero length"); + + // Check if the user already exists + + String userName = attr; + + if (hasUserAccounts() && getUserAccounts().findUser(userName) != null) + throw new AlfrescoRuntimeException("User " + userName + " already defined"); + + // Get the password for the account + + ConfigElement elem = user.getChild("password"); + if (elem == null) + throw new AlfrescoRuntimeException("No password specified for user " + userName); + + String password = elem.getValue(); + + // Create the user account + + UserAccount userAcc = new UserAccount(userName, password); + + // Check if the user in an administrator + + if (user.getChild("administrator") != null) + userAcc.setAdministrator(true); + + // Get the real user name and comment + + elem = user.getChild("realname"); + if (elem != null) + userAcc.setRealName(elem.getValue()); + + elem = user.getChild("comment"); + if (elem != null) + userAcc.setComment(elem.getValue()); + + // Add the user account + + UserAccountList accList = getUserAccounts(); + if (accList == null) + setUserAccounts(new UserAccountList()); + getUserAccounts().addUser(userAcc); + } + + /** + * Parse the platforms attribute returning the set of platform ids + * + * @param platformStr String + * @return EnumSet + */ + private final EnumSet parsePlatformString(String platformStr) + { + // Split the platform string and build up a set of platform types + + EnumSet platformTypes = EnumSet.noneOf(PlatformType.class); + if (platformStr == null || platformStr.length() == 0) + return platformTypes; + + StringTokenizer token = new StringTokenizer(platformStr.toUpperCase(), ","); + String typ = null; + + try + { + while (token.hasMoreTokens()) + { + + // Get the current platform type string and validate + + typ = token.nextToken().trim(); + PlatformType platform = PlatformType.valueOf(typ); + + if (platform != PlatformType.Unknown) + platformTypes.add(platform); + else + throw new AlfrescoRuntimeException("Invalid platform type, " + typ); + } + } + catch (IllegalArgumentException ex) + { + throw new AlfrescoRuntimeException("Invalid platform type, " + typ); + } + + // Return the platform types + + return platformTypes; + } + + /** + * Add a shared device to the server configuration. + * + * @param shr SharedDevice + * @return boolean + */ + public final boolean addShare(SharedDevice shr) + { + return m_shareList.addShare(shr); + } + + /** + * Add a server to the list of active servers + * + * @param srv NetworkServer + */ + public synchronized final void addServer(NetworkServer srv) + { + m_serverList.addServer(srv); + } + + /** + * Find an active server using the protocol name + * + * @param proto String + * @return NetworkServer + */ + public final NetworkServer findServer(String proto) + { + return m_serverList.findServer(proto); + } + + /** + * Remove an active server + * + * @param proto String + * @return NetworkServer + */ + public final NetworkServer removeServer(String proto) + { + return m_serverList.removeServer(proto); + } + + /** + * Return the number of active servers + * + * @return int + */ + public final int numberOfServers() + { + return m_serverList.numberOfServers(); + } + + /** + * Return the server at the specified index + * + * @param idx int + * @return NetworkServer + */ + public final NetworkServer getServer(int idx) + { + return m_serverList.getServer(idx); + } + + /** + * Check if there is an access control manager configured + * + * @return boolean + */ + public final boolean hasAccessControlManager() + { + return m_aclManager != null ? true : false; + } + + /** + * Get the access control manager that is used to control per share access + * + * @return AccessControlManager + */ + public final AccessControlManager getAccessControlManager() + { + return m_aclManager; + } + + /** + * Return the associated Acegi authentication manager + * + * @return AuthenticationManager + */ + public final AuthenticationManager getAuthenticationManager() + { + return acegiAuthMgr; + } + + /** + * Check if the global access control list is configured + * + * @return boolean + */ + public final boolean hasGlobalAccessControls() + { + return m_globalACLs != null ? true : false; + } + + /** + * Return the global access control list + * + * @return AccessControlList + */ + public final AccessControlList getGlobalAccessControls() + { + return m_globalACLs; + } + + /** + * Get the authenticator object that is used to provide user and share connection + * authentication. + * + * @return Authenticator + */ + public final SrvAuthenticator getAuthenticator() + { + return m_authenticator; + } + + /** + * Get the alfreso authentication service. + * + * @return + */ + public final AuthenticationService getAuthenticationService() + { + return authenticationService; + } + + /** + * Return the authentication component, for access to internal functions + * + * @return AuthenticationComponent + */ + public final AuthenticationComponent getAuthenticationComponent() + { + return m_authComponent; + } + + /** + * Return the node service + * + * @return NodeService + */ + public final NodeService getNodeService() + { + return m_nodeService; + } + + /** + * Return the person service + * + * @return PersonService + */ + public final PersonService getPersonService() + { + return m_personService; + } + + /** + * Return the transaction service + * + * @return TransactionService + */ + public final TransactionService getTransactionService() + { + return m_transactionService; + } + + /** + * Return the local address that the SMB server should bind to. + * + * @return java.net.InetAddress + */ + public final InetAddress getSMBBindAddress() + { + return m_smbBindAddress; + } + + /** + * Return the local address that the NetBIOS name server should bind to. + * + * @return java.net.InetAddress + */ + public final InetAddress getNetBIOSBindAddress() + { + return m_nbBindAddress; + } + + /** + * Return the network broadcast mask to be used for broadcast datagrams. + * + * @return java.lang.String + */ + public final String getBroadcastMask() + { + return m_broadcast; + } + + /** + * Return the server comment. + * + * @return java.lang.String + */ + public final String getComment() + { + return m_comment != null ? m_comment : ""; + } + + /** + * Return the disk interface to be used to create shares + * + * @return DiskInterface + */ + public final DiskInterface getDiskInterface() + { + return diskInterface; + } + + /** + * Return the domain name. + * + * @return java.lang.String + */ + public final String getDomainName() + { + return m_domain; + } + + /** + * Return the enabled SMB dialects that the server will use when negotiating sessions. + * + * @return DialectSelector + */ + public final DialectSelector getEnabledDialects() + { + return m_dialects; + } + + /** + * Return the server name. + * + * @return java.lang.String + */ + public final String getServerName() + { + return m_name; + } + + /** + * Return the server type flags. + * + * @return int + */ + public final int getServerType() + { + return m_srvType; + } + + /** + * Return the server debug flags. + * + * @return int + */ + public final int getSessionDebugFlags() + { + return m_sessDebug; + } + + /** + * Return the shared device list. + * + * @return SharedDeviceList + */ + public final SharedDeviceList getShares() + { + return m_shareList; + } + + /** + * Return the share mapper + * + * @return ShareMapper + */ + public final ShareMapper getShareMapper() + { + return m_shareMapper; + } + + /** + * Return the user account list. + * + * @return UserAccountList + */ + public final UserAccountList getUserAccounts() + { + return m_userList; + } + + /** + * Return the Win32 NetBIOS server name, if null the default server name will be used + * + * @return String + */ + public final String getWin32ServerName() + { + return m_win32NBName; + } + + /** + * Determine if the server should be announced via Win32 NetBIOS, so that it appears under + * Network Neighborhood. + * + * @return boolean + */ + public final boolean hasWin32EnableAnnouncer() + { + return m_win32NBAnnounce; + } + + /** + * Return the Win32 NetBIOS host announcement interval, in minutes + * + * @return int + */ + public final int getWin32HostAnnounceInterval() + { + return m_win32NBAnnounceInterval; + } + + /** + * Return the Win3 NetBIOS LANA number to use, or -1 for the first available + * + * @return int + */ + public final int getWin32LANA() + { + return m_win32NBLANA; + } + + /** + * Determine if the Win32 Netbios() API or Winsock Netbios calls should be used + * + * @return boolean + */ + public final boolean useWinsockNetBIOS() + { + return m_win32NBUseWinsock; + } + + /** + * Return the timezone name + * + * @return String + */ + public final String getTimeZone() + { + return m_timeZone; + } + + /** + * Return the timezone offset from UTC in seconds + * + * @return int + */ + public final int getTimeZoneOffset() + { + return m_tzOffset; + } + + /** + * Determine if the primary WINS server address has been set + * + * @return boolean + */ + public final boolean hasPrimaryWINSServer() + { + return m_winsPrimary != null ? true : false; + } + + /** + * Return the primary WINS server address + * + * @return InetAddress + */ + public final InetAddress getPrimaryWINSServer() + { + return m_winsPrimary; + } + + /** + * Determine if the secondary WINS server address has been set + * + * @return boolean + */ + public final boolean hasSecondaryWINSServer() + { + return m_winsSecondary != null ? true : false; + } + + /** + * Return the secondary WINS server address + * + * @return InetAddress + */ + public final InetAddress getSecondaryWINSServer() + { + return m_winsSecondary; + } + + /** + * Determine if the SMB server should bind to a particular local address + * + * @return boolean + */ + public final boolean hasSMBBindAddress() + { + return m_smbBindAddress != null ? true : false; + } + + /** + * Determine if the NetBIOS name server should bind to a particular local address + * + * @return boolean + */ + public final boolean hasNetBIOSBindAddress() + { + return m_nbBindAddress != null ? true : false; + } + + /** + * Determine if NetBIOS name server debugging is enabled + * + * @return boolean + */ + public final boolean hasNetBIOSDebug() + { + return m_nbDebug; + } + + /** + * Determine if host announcement debugging is enabled + * + * @return boolean + */ + public final boolean hasHostAnnounceDebug() + { + return m_announceDebug; + } + + /** + * Determine if the server should be announced so that it appears under Network Neighborhood. + * + * @return boolean + */ + public final boolean hasEnableAnnouncer() + { + return m_announce; + } + + /** + * Return the host announcement interval, in minutes + * + * @return int + */ + public final int getHostAnnounceInterval() + { + return m_announceInterval; + } + + /** + * Return the JCE provider class name + * + * @return String + */ + public final String getJCEProvider() + { + return m_jceProviderClass; + } + + /** + * Get the local server name and optionally trim the domain name + * + * @param trimDomain boolean + * @return String + */ + public final String getLocalServerName(boolean trimDomain) + { + // Check if the name has already been set + + if (m_localName != null) + return m_localName; + + // Find the local server name + + String srvName = null; + + if (getPlatformType() == PlatformType.WINDOWS) + { + // Get the local name via JNI + + srvName = Win32NetBIOS.GetLocalNetBIOSName(); + } + else + { + // Get the DNS name of the local system + + try + { + srvName = InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException ex) + { + } + } + + // Strip the domain name + + if (trimDomain && srvName != null) + { + int pos = srvName.indexOf("."); + if (pos != -1) + srvName = srvName.substring(0, pos); + } + + // Save the local server name + + m_localName = srvName; + + // Return the local server name + + return srvName; + } + + /** + * Get the local domain/workgroup name + * + * @return String + */ + public final String getLocalDomainName() + { + // Check if the local domain has been set + + if (m_localDomain != null) + return m_localDomain; + + // Find the local domain name + + String domainName = null; + + if (getPlatformType() == PlatformType.WINDOWS) + { + // Get the local domain/workgroup name via JNI + + domainName = Win32NetBIOS.GetLocalDomainName(); + + // Debug + + if (logger.isDebugEnabled()) + logger.debug("Local domain name is " + domainName + " (via JNI)"); + } + else + { + NetBIOSName nbName = null; + + try + { + // Try and find the browse master on the local network + + nbName = NetBIOSSession.FindName(NetBIOSName.BrowseMasterName, NetBIOSName.BrowseMasterGroup, 5000); + + // Log the browse master details + + if (logger.isDebugEnabled()) + logger.debug("Found browse master at " + nbName.getIPAddressString(0)); + + // Get the NetBIOS name list from the browse master + + NetBIOSNameList nbNameList = NetBIOSSession.FindNamesForAddress(nbName.getIPAddressString(0)); + nbName = nbNameList.findName(NetBIOSName.MasterBrowser, false); + + // Set the domain/workgroup name + + if (nbName != null) + domainName = nbName.getName(); + else + throw new AlfrescoRuntimeException("Failed to find local domain/workgroup name"); + } + catch (IOException ex) + { + throw new AlfrescoRuntimeException("Failed to determine local domain/workgroup"); + } + } + + // Save the local domain name + + m_localDomain = domainName; + + // Return the local domain/workgroup name + + return domainName; + } + + /** + * Return the primary filesystem shared device, or null if not available + * + * @return DiskSharedDevice + */ + public final DiskSharedDevice getPrimaryFilesystem() + { + // Check if there are any global shares defined + + SharedDeviceList shares = getShares(); + DiskSharedDevice diskShare = null; + + if ( shares != null && shares.numberOfShares() > 0) + { + // Find the first available filesystem device + + Enumeration shareEnum = shares.enumerateShares(); + + while ( diskShare == null && shareEnum.hasMoreElements()) + { + SharedDevice curShare = shareEnum.nextElement(); + if ( curShare.getType() == ShareType.DISK) + diskShare = (DiskSharedDevice) curShare; + } + } + + // Return the first filesystem device, or null + + return diskShare; + } + + /** + * Determine if Macintosh extension SMBs are enabled + * + * @return boolean + */ + public final boolean hasMacintoshExtensions() + { + return m_macExtensions; + } + + /** + * Determine if there are any user accounts defined. + * + * @return boolean + */ + public final boolean hasUserAccounts() + { + if (m_userList != null && m_userList.numberOfUsers() > 0) + return true; + return false; + } + + /** + * Determine if NetBIOS SMB is enabled + * + * @return boolean + */ + public final boolean hasNetBIOSSMB() + { + return m_netBIOSEnable; + } + + /** + * Determine if TCP/IP SMB is enabled + * + * @return boolean + */ + public final boolean hasTcpipSMB() + { + return m_tcpSMBEnable; + } + + /** + * Determine if Win32 NetBIOS is enabled + * + * @return boolean + */ + public final boolean hasWin32NetBIOS() + { + return m_win32NBEnable; + } + + /** + * Check if the SMB server is enabled + * + * @return boolean + */ + public final boolean isSMBServerEnabled() + { + return m_smbEnable; + } + + /** + * Set the SMB server enabled state + * + * @param ena boolean + */ + public final void setSMBServerEnabled(boolean ena) + { + m_smbEnable = ena; + } + + /** + * Set the FTP server enabled state + * + * @param ena boolean + */ + public final void setFTPServerEnabled(boolean ena) + { + m_ftpEnable = ena; + } + + /** + * Set the authenticator to be used to authenticate users and share connections. + * + * @param auth SrvAuthenticator + * @param params ConfigElement + * @param allowGuest boolean + */ + public final void setAuthenticator(SrvAuthenticator auth, ConfigElement params, boolean allowGuest) + { + + // Set the server authenticator mode and guest access + + auth.setAccessMode(SrvAuthenticator.USER_MODE); + auth.setAllowGuest(allowGuest); + + // Initialize the authenticator using the parameter values + + try + { + auth.initialize(this, params); + } + catch (InvalidConfigurationException ex) + { + throw new AlfrescoRuntimeException("Failed to initialize authenticator", ex); + } + + // Set the server authenticator and initialization parameters + + m_authenticator = auth; + } + + /** + * Set the local address that the SMB server should bind to. + * + * @param addr InetAddress + */ + public final void setSMBBindAddress(InetAddress addr) + { + m_smbBindAddress = addr; + } + + /** + * Set the local address that the NetBIOS name server should bind to. + * + * @param addr InetAddress + */ + public final void setNetBIOSBindAddress(InetAddress addr) + { + m_nbBindAddress = addr; + } + + /** + * Set the broadcast mask to be used for broadcast datagrams. + * + * @param mask String + */ + public final void setBroadcastMask(String mask) + { + m_broadcast = mask; + + // Copy settings to the NetBIOS session class + + NetBIOSSession.setSubnetMask(mask); + } + + /** + * Set the server comment. + * + * @param comment String + */ + public final void setComment(String comment) + { + m_comment = comment; + } + + /** + * Set the domain that the server belongs to. + * + * @param domain String + */ + public final void setDomainName(String domain) + { + m_domain = domain; + } + + /** + * Enable/disable the host announcer. + * + * @param b boolean + */ + public final void setHostAnnouncer(boolean b) + { + m_announce = b; + } + + /** + * Set the host announcement interval, in minutes + * + * @param ival int + */ + public final void setHostAnnounceInterval(int ival) + { + m_announceInterval = ival; + } + + /** + * Set the JCE provider + * + * @param providerClass String + */ + public final void setJCEProvider(String providerClass) + { + + // Validate the JCE provider class + + try + { + + // Load the JCE provider class and validate + + Object jceObj = Class.forName(providerClass).newInstance(); + if (jceObj instanceof java.security.Provider) + { + + // Inform listeners, validate the configuration change + + Provider jceProvider = (Provider) jceObj; + + // Save the JCE provider class name + + m_jceProviderClass = providerClass; + + // Add the JCE provider + + Security.addProvider(jceProvider); + } + else + { + throw new AlfrescoRuntimeException("JCE provider class is not a valid Provider class"); + } + } + catch (ClassNotFoundException ex) + { + throw new AlfrescoRuntimeException("JCE provider class " + providerClass + " not found"); + } + catch (Exception ex) + { + throw new AlfrescoRuntimeException("JCE provider class error", ex); + } + } + + /** + * Enable/disable NetBIOS name server debug output + * + * @param ena boolean + */ + public final void setNetBIOSDebug(boolean ena) + { + m_nbDebug = ena; + } + + /** + * Enable/disable host announcement debug output + * + * @param ena boolean + */ + public final void setHostAnnounceDebug(boolean ena) + { + m_announceDebug = ena; + } + + /** + * Set the server name. + * + * @param name String + */ + public final void setServerName(String name) + { + m_name = name; + } + + /** + * Set the debug flags to be used by the server. + * + * @param flags int + */ + public final void setSessionDebugFlags(int flags) + { + m_sessDebug = flags; + } + + /** + * Set the user account list. + * + * @param users UserAccountList + */ + public final void setUserAccounts(UserAccountList users) + { + m_userList = users; + } + + /** + * Set the global access control list + * + * @param acls AccessControlList + */ + public final void setGlobalAccessControls(AccessControlList acls) + { + m_globalACLs = acls; + } + + /** + * Enable/disable the NetBIOS SMB support + * + * @param ena boolean + */ + public final void setNetBIOSSMB(boolean ena) + { + m_netBIOSEnable = ena; + } + + /** + * Enable/disable the TCP/IP SMB support + * + * @param ena boolean + */ + public final void setTcpipSMB(boolean ena) + { + m_tcpSMBEnable = ena; + } + + /** + * Enable/disable the Win32 NetBIOS SMB support + * + * @param ena boolean + */ + public final void setWin32NetBIOS(boolean ena) + { + m_win32NBEnable = ena; + } + + /** + * Set the Win32 NetBIOS file server name + * + * @param name String + */ + public final void setWin32NetBIOSName(String name) + { + m_win32NBName = name; + } + + /** + * Enable/disable the Win32 NetBIOS host announcer. + * + * @param b boolean + */ + public final void setWin32HostAnnouncer(boolean b) + { + m_win32NBAnnounce = b; + } + + /** + * Set the Win32 LANA to be used by the Win32 NetBIOS interface + * + * @param ival int + */ + public final void setWin32LANA(int ival) + { + m_win32NBLANA = ival; + } + + /** + * Set the Win32 NetBIOS host announcement interval, in minutes + * + * @param ival int + */ + public final void setWin32HostAnnounceInterval(int ival) + { + m_win32NBAnnounceInterval = ival; + } + + /** + * Set the Win32 NetBIOS interface to use either Winsock NetBIOS or the Netbios() API calls + * + * @param useWinsock boolean + */ + public final void setWin32WinsockNetBIOS(boolean useWinsock) + { + m_win32NBUseWinsock = useWinsock; + } + + /** + * Set the server timezone name + * + * @param name String + * @exception InvalidConfigurationException If the timezone is invalid + */ + public final void setTimeZone(String name) throws InvalidConfigurationException + { + + // Validate the timezone + + TimeZone tz = TimeZone.getTimeZone(name); + if (tz == null) + throw new InvalidConfigurationException("Invalid timezone, " + name); + + // Set the timezone name and offset from UTC in minutes + // + // Invert the result of TimeZone.getRawOffset() as SMB/CIFS requires + // positive minutes west of UTC + + m_timeZone = name; + m_tzOffset = -(tz.getRawOffset() / 60000); + } + + /** + * Set the timezone offset from UTC in seconds (+/-) + * + * @param offset int + */ + public final void setTimeZoneOffset(int offset) + { + m_tzOffset = offset; + } + + /** + * Set the primary WINS server address + * + * @param addr InetAddress + */ + public final void setPrimaryWINSServer(InetAddress addr) + { + m_winsPrimary = addr; + } + + /** + * Set the secondary WINS server address + * + * @param addr InetAddress + */ + public final void setSecondaryWINSServer(InetAddress addr) + { + m_winsSecondary = addr; + } + + /** + * Check if the FTP server is enabled + * + * @return boolean + */ + public final boolean isFTPServerEnabled() + { + return m_ftpEnable; + } + + /** + * Return the FTP server bind address, may be null to indicate bind to all available addresses + * + * @return InetAddress + */ + public final InetAddress getFTPBindAddress() + { + return m_ftpBindAddress; + } + + /** + * Return the FTP server port to use for incoming connections + * + * @return int + */ + public final int getFTPPort() + { + return m_ftpPort; + } + + /** + * Determine if anonymous FTP access is allowed + * + * @return boolean + */ + public final boolean allowAnonymousFTP() + { + return m_ftpAllowAnonymous; + } + + /** + * Return the anonymous FTP account name + * + * @return String + */ + public final String getAnonymousFTPAccount() + { + return m_ftpAnonymousAccount; + } + + /** + * Return the FTP debug flags + * + * @return int + */ + public final int getFTPDebug() + { + return m_ftpDebug; + } + + /** + * Check if an FTP root path has been configured + * + * @return boolean + */ + public final boolean hasFTPRootPath() + { + return m_ftpRootPath != null ? true : false; + } + + /** + * Return the FTP root path + * + * @return String + */ + public final String getFTPRootPath() + { + return m_ftpRootPath; + } + + /** + * Set the FTP server bind address, may be null to indicate bind to all available addresses + * + * @param addr InetAddress + */ + public final void setFTPBindAddress(InetAddress addr) + { + m_ftpBindAddress = addr; + } + + /** + * Set the FTP server port to use for incoming connections, -1 indicates disable the FTP server + * + * @param port int + */ + public final void setFTPPort(int port) + { + m_ftpPort = port; + } + + /** + * Set the FTP root path + * + * @param path String + */ + public final void setFTPRootPath(String path) + { + m_ftpRootPath = path; + } + + /** + * Enable/disable anonymous FTP access + * + * @param ena boolean + */ + public final void setAllowAnonymousFTP(boolean ena) + { + m_ftpAllowAnonymous = ena; + } + + /** + * Set the anonymous FTP account name + * + * @param acc String + */ + public final void setAnonymousFTPAccount(String acc) + { + m_ftpAnonymousAccount = acc; + } + + /** + * Set the FTP debug flags + * + * @param dbg int + */ + public final void setFTPDebug(int dbg) + { + m_ftpDebug = dbg; + } + + /** + * Close the server configuration, used to close various components that are shared between protocol + * handlers. + */ + public final void closeConfiguration() + { + // Close the authenticator + + if ( getAuthenticator() != null) + { + getAuthenticator().closeAuthenticator(); + m_authenticator = null; + } + + // Close the shared filesystems + + if ( getShares() != null && getShares().numberOfShares() > 0) + { + // Close the shared filesystems + + Enumeration shareEnum = getShares().enumerateShares(); + + while ( shareEnum.hasMoreElements()) + { + SharedDevice share = shareEnum.nextElement(); + DeviceContext devCtx = share.getContext(); + + if ( devCtx != null) + devCtx.CloseContext(); + } + } + } + + /** + * Load an authenticator using dyanmic loading + * + * @param className String + * @return SrvAuthenticator + */ + private final SrvAuthenticator loadAuthenticatorClass(String className) + { + SrvAuthenticator srvAuth = null; + + try + { + // Load the authenticator class + + Object authObj = Class.forName(className).newInstance(); + + // Verify that the class is an authenticator + + if ( authObj instanceof SrvAuthenticator) + srvAuth = (SrvAuthenticator) authObj; + } + catch (Exception ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Failed to load authenticator class " + className); + } + + // Return the authenticator class, or null if not available or invalid + + return srvAuth; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/core/DeviceContext.java b/source/java/org/alfresco/filesys/server/core/DeviceContext.java new file mode 100644 index 0000000000..422eeb826d --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/DeviceContext.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +/** + *

    + * The device context is passed to the methods of a device interface. Each shared device has a + * device interface and a device context associated with it. The device context allows a single + * device interface to be used for multiple shared devices. + */ +public class DeviceContext +{ + + // Device name that the interface is associated with + + private String m_devName; + + // Flag to indicate if the device is available. Unavailable devices will not be listed by the + // various + // protocol servers. + + private boolean m_available = true; + + /** + * DeviceContext constructor. + */ + public DeviceContext() + { + super(); + } + + /** + * DeviceContext constructor. + */ + public DeviceContext(String devName) + { + m_devName = devName; + } + + /** + * Return the device name. + * + * @return java.lang.String + */ + public final String getDeviceName() + { + return m_devName; + } + + /** + * Determine if the filesystem is available + * + * @return boolean + */ + public final boolean isAvailable() + { + return m_available; + } + + /** + * Set the filesystem as available, or not + * + * @param avail boolean + */ + public final void setAvailable(boolean avail) + { + m_available = avail; + } + + /** + * Set the device name. + * + * @param name java.lang.String + */ + public final void setDeviceName(String name) + { + m_devName = name; + } + + /** + * Close the device context, free any resources allocated by the context + */ + public void CloseContext() + { + } + + /** + * Return the context as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getDeviceName()); + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/core/DeviceContextException.java b/source/java/org/alfresco/filesys/server/core/DeviceContextException.java new file mode 100644 index 0000000000..399335d460 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/DeviceContextException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +/** + * Device Context Exception Class + *

    + * Thrown when a device context parameter string is invalid. + */ +public class DeviceContextException extends Exception +{ + private static final long serialVersionUID = 3761124938182244658L; + + /** + * Class constructor + */ + public DeviceContextException() + { + super(); + } + + /** + * Class constructor + * + * @param s java.lang.String + */ + public DeviceContextException(String s) + { + super(s); + } + +} diff --git a/source/java/org/alfresco/filesys/server/core/DeviceInterface.java b/source/java/org/alfresco/filesys/server/core/DeviceInterface.java new file mode 100644 index 0000000000..54af951dc2 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/DeviceInterface.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.filesys.TreeConnection; + +/** + * The device interface is the base of the shared device interfaces that are used by shared devices + * on the SMB server. + */ +public interface DeviceInterface +{ + + /** + * Parse and validate the parameter string and create a device context object for this instance + * of the shared device. The same DeviceInterface implementation may be used for multiple + * shares. + * + * @param args ConfigElement + * @return DeviceContext + * @exception DeviceContextException + */ + public DeviceContext createContext(ConfigElement args) throws DeviceContextException; + + /** + * Connection opened to this disk device + * + * @param sess Server session + * @param tree Tree connection + */ + public void treeOpened(SrvSession sess, TreeConnection tree); + + /** + * Connection closed to this device + * + * @param sess Server session + * @param tree Tree connection + */ + public void treeClosed(SrvSession sess, TreeConnection tree); +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/core/InvalidDeviceInterfaceException.java b/source/java/org/alfresco/filesys/server/core/InvalidDeviceInterfaceException.java new file mode 100644 index 0000000000..4dcdb6af57 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/InvalidDeviceInterfaceException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +/** + *

    + * This exception may be thrown by a SharedDevice when the device interface has not been specified, + * the device interface does not match the shared device type, or the device interface driver class + * cannot be loaded. + */ +public class InvalidDeviceInterfaceException extends Exception +{ + private static final long serialVersionUID = 3834029177581222198L; + + /** + * InvalidDeviceInterfaceException constructor. + */ + public InvalidDeviceInterfaceException() + { + super(); + } + + /** + * InvalidDeviceInterfaceException constructor. + * + * @param s java.lang.String + */ + public InvalidDeviceInterfaceException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/core/ShareMapper.java b/source/java/org/alfresco/filesys/server/core/ShareMapper.java new file mode 100644 index 0000000000..2e9da5de2a --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/ShareMapper.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; + +/** + * Share Mapper Interface + *

    + * The share mapper interface is used to allocate a share of the specified name and type. It is + * called by the SMB server to allocate disk and print type shares. + */ +public interface ShareMapper +{ + + /** + * Initialize the share mapper + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initializeMapper(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException; + + /** + * Return the share list for the specified host. The host name can be used to implement virtual + * hosts. + * + * @param host + * @param sess SrvSession + * @param allShares boolean + * @return SharedDeviceList + */ + public SharedDeviceList getShareList(String host, SrvSession sess, boolean allShares); + + /** + * Find the share of the specified name/type + * + * @param tohost String + * @param name String + * @param typ int + * @param sess SrvSession + * @param create boolean + * @return SharedDevice + * @exception Exception + */ + public SharedDevice findShare(String tohost, String name, int typ, SrvSession sess, boolean create) + throws Exception; + + /** + * Delete any temporary shares created for the specified session + * + * @param sess SrvSession + */ + public void deleteShares(SrvSession sess); + + /** + * Close the share mapper, release any resources. Called when the server is shutting down. + */ + public void closeMapper(); +} diff --git a/source/java/org/alfresco/filesys/server/core/ShareType.java b/source/java/org/alfresco/filesys/server/core/ShareType.java new file mode 100644 index 0000000000..ffe9e34e27 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/ShareType.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +/** + *

    + * Available shared resource types. + */ +public class ShareType +{ + // Disk share resource type. + + public static final int DISK = 0; + + // Printer share resource type. + + public static final int PRINTER = 1; + + // Named pipe/IPC share resource type. + + public static final int NAMEDPIPE = 2; + + // Remote administration named pipe, IPC$ + + public static final int ADMINPIPE = 3; + + // Unknown share type + + public static final int UNKNOWN = -1; + + /** + * Return the share type as a share information type. + * + * @return int + * @param typ int + */ + public final static int asShareInfoType(int typ) + { + + // Convert the share type value to a valid share information structure share type + // value. + + int shrTyp = 0; + + switch (typ) + { + case DISK: + shrTyp = 0; + break; + case PRINTER: + shrTyp = 1; + break; + case NAMEDPIPE: + case ADMINPIPE: + shrTyp = 3; + break; + } + return shrTyp; + } + + /** + * Return the SMB service name as a shared device type. + * + * @return int + * @param srvName java.lang.String + */ + public final static int ServiceAsType(String srvName) + { + + // Check the service name + + if (srvName.compareTo("A:") == 0) + return DISK; + else if (srvName.compareTo("LPT1:") == 0) + return PRINTER; + else if (srvName.compareTo("IPC") == 0) + return NAMEDPIPE; + + // Unknown service name string + + return UNKNOWN; + } + + /** + * Return the share type as a service string. + * + * @return java.lang.String + * @param typ int + */ + public final static String TypeAsService(int typ) + { + + if (typ == DISK) + return "A:"; + else if (typ == PRINTER) + return "LPT1:"; + else if (typ == NAMEDPIPE || typ == ADMINPIPE) + return "IPC"; + return ""; + } + + /** + * Return the share type as a string. + * + * @return java.lang.String + * @param typ int + */ + public final static String TypeAsString(int typ) + { + + if (typ == DISK) + return "DISK"; + else if (typ == PRINTER) + return "PRINT"; + else if (typ == NAMEDPIPE) + return "PIPE"; + else if (typ == ADMINPIPE) + return "IPC$"; + return ""; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/core/SharedDevice.java b/source/java/org/alfresco/filesys/server/core/SharedDevice.java new file mode 100644 index 0000000000..61df75a120 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/SharedDevice.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +import org.alfresco.filesys.server.auth.acl.AccessControl; +import org.alfresco.filesys.server.auth.acl.AccessControlList; + +/** + *

    + * The shared device class is the base class for all shared device implementations. + */ +public class SharedDevice implements Comparable +{ + // Share attribute types + + public static final int Admin = 0x0001; + public static final int Hidden = 0x0002; + public static final int ReadOnly = 0x0004; + public static final int Temporary = 0x0008; + + // Shared device name + + private String m_name; + + // Shared device type + + private int m_type; + + // Comment + + private String m_comment; + + // Device interface and context object + + private DeviceInterface m_interface; + private DeviceContext m_drvCtx; + + // Share attributes + + private int m_attrib; + + // Current and maximum connections to this shared device + + private int m_maxUses = -1; // unlimited + private int m_curUses; + + // Access control list + + private AccessControlList m_acls; + + /** + * SharedDevice constructor. + * + * @param name Shared device name. + * @param typ Share device type, as specified by class ShareType. + * @param ctx Context object that will be passed to the interface. + */ + protected SharedDevice(String name, int typ, DeviceContext ctx) + { + + // Set the shared name and device type + + setName(name); + setType(typ); + setContext(ctx); + } + + /** + * Return the shared device attribtues. + * + * @return int + */ + public final int getAttributes() + { + return m_attrib; + } + + /** + * Determine if the shared device has any access controls configured + * + * @return boolean + */ + public final boolean hasAccessControls() + { + if (m_acls == null) + return false; + return true; + } + + /** + * Return the access control list + * + * @return AccessControlList + */ + public final AccessControlList getAccessControls() + { + return m_acls; + } + + /** + * Check if the shared device has a comment + * + * @return boolean + */ + public final boolean hasComment() + { + return m_comment != null ? true : false; + } + + /** + * Return the shared device comment. + * + * @return java.lang.String + */ + public final String getComment() + { + return m_comment; + } + + /** + * Return the device interface specific context object. + * + * @return Device context. + */ + public final DeviceContext getContext() + { + return m_drvCtx; + } + + /** + * Return the device interface for this shared device. + * + * @return DeviceInterface + */ + public DeviceInterface getInterface() throws InvalidDeviceInterfaceException + { + return m_interface; + } + + /** + * Return the shared device name. + * + * @return java.lang.String + */ + public final String getName() + { + return m_name; + } + + /** + * Return the shared device type, as specified by the ShareType class. + * + * @return int + */ + public int getType() + { + return m_type; + } + + /** + * Return the current connection count for the share + * + * @return int + */ + public final int getCurrentConnectionCount() + { + return m_curUses; + } + + /** + * Return the maximum connection count for the share + * + * @return int + */ + public final int getMaximumConnectionCount() + { + return m_maxUses; + } + + /** + * Generates a hash code for the receiver. This method is supported primarily for hash tables, + * such as those provided in java.util. + * + * @return an integer hash code for the receiver + * @see java.util.Hashtable + */ + public int hashCode() + { + + // Use the share name to generate the hash code. + + return getName().hashCode(); + } + + /** + * Determine if this is an admin share. + * + * @return boolean + */ + public final boolean isAdmin() + { + return (m_attrib & Admin) == 0 ? false : true; + } + + /** + * Determine if this is a hidden share. + * + * @return boolean + */ + public final boolean isHidden() + { + return (m_attrib & Hidden) == 0 ? false : true; + } + + /** + * Determine if the share is read-only. + * + * @return boolean + */ + public final boolean isReadOnly() + { + return (m_attrib & ReadOnly) == 0 ? false : true; + } + + /** + * Determine if the share is a temporary share + * + * @return boolean + */ + public final boolean isTemporary() + { + return (m_attrib & Temporary) == 0 ? false : true; + } + + /** + * Set the shared device comment string. + * + * @param comm java.lang.String + */ + public final void setComment(String comm) + { + m_comment = comm; + } + + /** + * Set the shared device attributes. + * + * @param attr int + */ + public final void setAttributes(int attr) + { + m_attrib = attr; + } + + /** + * Set the context that is passed to the device interface. + * + * @param ctx DeviceContext + */ + protected void setContext(DeviceContext ctx) + { + m_drvCtx = ctx; + } + + /** + * Set the device interface for this shared device. + * + * @param iface DeviceInterface + */ + protected final void setInterface(DeviceInterface iface) + { + m_interface = iface; + } + + /** + * Set the shared device name. + * + * @param name java.lang.String Shared device name. + */ + protected final void setName(String name) + { + m_name = name; + } + + /** + * Set the shared device type. + * + * @param typ int Shared device type, as specified by class ShareType. + */ + protected final void setType(int typ) + { + m_type = typ; + } + + /** + * Set the maximum connection coutn for this shared device + * + * @param maxConn int + */ + public final void setMaximumConnectionCount(int maxConn) + { + m_maxUses = maxConn; + } + + /** + * Set the access control list using the specified list + * + * @param acls AccessControlList + */ + public final void setAccessControlList(AccessControlList acls) + { + m_acls = acls; + } + + /** + * Add an access control to the shared device + * + * @param acl AccessControl + */ + public final void addAccessControl(AccessControl acl) + { + + // Check if the access control list has been allocated + + if (m_acls == null) + m_acls = new AccessControlList(); + + // Add the access control + + m_acls.addControl(acl); + } + + /** + * Remove an access control + * + * @param idx int + * @return AccessControl + */ + public final AccessControl removeAccessControl(int idx) + { + + // validate the index + + if (m_acls == null || idx < 0 || idx >= m_acls.numberOfControls()) + return null; + + // Remove the access control + + return m_acls.removeControl(idx); + } + + /** + * Remove all access controls from this shared device + */ + public final void removeAllAccessControls() + { + if (m_acls != null) + { + m_acls.removeAllControls(); + m_acls = null; + } + } + + /** + * Parse and validate the parameters string and create a device context for the shared device. + * + * @param args String[] + * @return DeviceContext + */ + public DeviceContext createContext(String[] args) + { + return new DeviceContext(args[0]); + } + + /** + * Increment the connection count for the share + */ + public synchronized void incrementConnectionCount() + { + m_curUses++; + } + + /** + * Decrement the connection count for the share + */ + public synchronized void decrementConnectionCount() + { + m_curUses--; + } + + /** + * Compare this shared device to another shared device using the device name + * + * @param obj Object + */ + public int compareTo(Object obj) + { + if (obj instanceof SharedDevice) + { + SharedDevice sd = (SharedDevice) obj; + return getName().compareTo(sd.getName()); + } + return -1; + } + + /** + * Compares two objects for equality. Returns a boolean that indicates whether this object is + * equivalent to the specified object. This method is used when an object is stored in a + * hashtable. + * + * @param obj the Object to compare with + * @return true if these Objects are equal; false otherwise. + * @see java.util.Hashtable + */ + public boolean equals(Object obj) + { + + // Check if the object is a SharedDevice + + if (obj instanceof SharedDevice) + { + + // Check if the share names are equal + + SharedDevice shr = (SharedDevice) obj; + if (getName().compareTo(shr.getName()) == 0) + return true; + } + + // Object type, or share name is not equal + + return false; + } + + /** + * Returns a String that represents the value of this object. + * + * @return a string representation of the receiver + */ + public String toString() + { + + // Build a string that represents this shared device + + StringBuffer str = new StringBuffer(); + str.append("["); + str.append(getName()); + str.append(","); + str.append(ShareType.TypeAsString(getType())); + str.append(","); + + if (hasAccessControls()) + { + str.append("ACLs="); + str.append(m_acls.numberOfControls()); + } + + if (isAdmin()) + str.append(",Admin"); + + if (isHidden()) + str.append(",Hidden"); + + if (isReadOnly()) + str.append(",ReadOnly"); + + if (isTemporary()) + str.append(",Temp"); + + if (getContext() != null && getContext().isAvailable() == false) + str.append(",Offline"); + + if (m_drvCtx != null) + { + str.append(","); + str.append(m_drvCtx.toString()); + } + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/core/SharedDeviceList.java b/source/java/org/alfresco/filesys/server/core/SharedDeviceList.java new file mode 100644 index 0000000000..d8ae195dde --- /dev/null +++ b/source/java/org/alfresco/filesys/server/core/SharedDeviceList.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.core; + +import java.util.Enumeration; +import java.util.Hashtable; + +/** + *

    + * List of shared devices. + */ +public class SharedDeviceList +{ + + // Shared device list + + private Hashtable m_shares; + + /** + * SharedDeviceList constructor. + */ + public SharedDeviceList() + { + + // Allocate the shared device list + + m_shares = new Hashtable(); + } + + /** + * Copy constructor + * + * @param shrList SharedDeviceList + */ + public SharedDeviceList(SharedDeviceList shrList) + { + + // Allocate the shared device list + + m_shares = new Hashtable(); + + // Copy the shares from the original list, shallow copy + + addShares(shrList); + } + + /** + * Add a shared device to the list. + * + * @param shr Shared device to be added to the list. + * @return True if the share was added successfully, else false. + */ + public final boolean addShare(SharedDevice shr) + { + + // Check if a share with the specified name already exists + + if (m_shares.containsKey(shr.getName())) + return false; + + // Add the shared device + + m_shares.put(shr.getName(), shr); + return true; + } + + /** + * Add shares from the specified list to this list, using a shallow copy + * + * @param shrList SharedDeviceList + */ + public final void addShares(SharedDeviceList shrList) + { + + // Copy the shares to this list + + Enumeration enm = shrList.enumerateShares(); + + while (enm.hasMoreElements()) + addShare(enm.nextElement()); + } + + /** + * Delete the specified shared device from the list. + * + * @param name String Name of the shared resource to remove from the list. + * @return SharedDevice that has been removed from the list, else null. + */ + public final SharedDevice deleteShare(String name) + { + + // Remove the shared device from the list + + return (SharedDevice) m_shares.remove(name); + } + + /** + * Return an enumeration to allow the shared devices to be listed. + * + * @return Enumeration + */ + public final Enumeration enumerateShares() + { + return m_shares.elements(); + } + + /** + * Find the shared device with the specified name. + * + * @param name Name of the shared device to find. + * @return SharedDevice with the specified name, else null. + */ + public final SharedDevice findShare(String name) + { + return m_shares.get(name); + } + + /** + * Find the shared device with the specified name and type + * + * @param name Name of shared device to find + * @param typ Type of shared device (see ShareType) + * @param nocase Case sensitive search if false, else case insensitive search + * @return SharedDevice with the specified name and type, else null + */ + public final SharedDevice findShare(String name, int typ, boolean nocase) + { + + // Enumerate the share list + + Enumeration keys = m_shares.keys(); + + while (keys.hasMoreElements()) + { + + // Get the current share name + + String curName = keys.nextElement(); + + if ((nocase == false && curName.equals(name)) || (nocase == true && curName.equalsIgnoreCase(name))) + { + + // Get the shared device and check if the share is of the required type + + SharedDevice share = (SharedDevice) m_shares.get(curName); + if (share.getType() == typ || typ == ShareType.UNKNOWN) + return share; + } + } + + // Required share not found + + return null; + } + + /** + * Return the number of shared devices in the list. + * + * @return int + */ + public final int numberOfShares() + { + return m_shares.size(); + } + + /** + * Remove shares that have an unavailable status from the list + * + * @return int + */ + public final int removeUnavailableShares() + { + + // Check if any shares are unavailable + + Enumeration shrEnum = enumerateShares(); + int remCnt = 0; + + while (shrEnum.hasMoreElements()) + { + + // Check if the current share is unavailable + + SharedDevice shr = shrEnum.nextElement(); + if (shr.getContext() != null && shr.getContext().isAvailable() == false) + { + deleteShare(shr.getName()); + remCnt++; + } + } + + // Return the count of shares removed + + return remCnt; + } + + /** + * Remove all shared devices from the share list + */ + public final void removeAllShares() + { + m_shares.clear(); + } + + /** + * Return the share list as a string + * + * @return String + */ + public String toString() + { + + // Create a buffer to build the string + + StringBuffer str = new StringBuffer(); + str.append("["); + + // Enumerate the shares + + Enumeration enm = m_shares.keys(); + + while (enm.hasMoreElements()) + { + String name = enm.nextElement(); + str.append(name); + str.append(","); + } + + // Remove the trailing comma + + if (str.length() > 1) + str.setLength(str.length() - 1); + str.append("]"); + + // Return the string + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/AccessDeniedException.java b/source/java/org/alfresco/filesys/server/filesys/AccessDeniedException.java new file mode 100644 index 0000000000..742ede787f --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/AccessDeniedException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * Thrown when an attempt is made to write to a file that is read-only or the user only has read + * access to, or open a file that is actually a directory. + */ +public class AccessDeniedException extends java.io.IOException +{ + private static final long serialVersionUID = 3688785881968293433L; + + /** + * AccessDeniedException constructor + */ + public AccessDeniedException() + { + super(); + } + + /** + * AccessDeniedException constructor. + * + * @param s java.lang.String + */ + public AccessDeniedException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/AccessMode.java b/source/java/org/alfresco/filesys/server/filesys/AccessMode.java new file mode 100644 index 0000000000..082d1c5884 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/AccessMode.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * SMB file access mode class. + *

    + * The SMB access mode values are used when opening a file using one of the SMBDiskSession OpenFile + * (), OpenInputStream () or OpenOutputStream () methods. + */ +public final class AccessMode +{ + + // Access mode constants + + public static final int ReadOnly = 0x0000; + public static final int WriteOnly = 0x0001; + public static final int ReadWrite = 0x0002; + public static final int Execute = 0x0003; + public static final int Compatability = 0x0000; + public static final int Exclusive = 0x0010; + public static final int DenyWrite = 0x0020; + public static final int DenyRead = 0x0030; + public static final int DenyNone = 0x0040; + public static final int NoCaching = 0x1000; + public static final int WriteThrough = 0x4000; + protected static final int FCBOpen = 0x00FF; + + // NT access mode constants + + public static final int NTRead = 0x00000001; + public static final int NTWrite = 0x00000002; + public static final int NTAppend = 0x00000004; + public static final int NTReadEA = 0x00000008; + public static final int NTWriteEA = 0x00000010; + public static final int NTExecute = 0x00000020; + public static final int NTDeleteChild = 0x00000040; + public static final int NTReadAttrib = 0x00000080; + public static final int NTWriteAttrib = 0x00000100; + + public static final int NTDelete = 0x00010000; + public static final int NTReadControl = 0x00020000; + public static final int NTWriteDAC = 0x00040000; + public static final int NTWriteOwner = 0x00080000; + public static final int NTSynchronize = 0x00100000; + public static final int NTSystemSecurity = 0x01000000; + + public static final int NTGenericRead = 0x80000000; + public static final int NTGenericWrite = 0x40000000; + public static final int NTGenericExecute = 0x20000000; + public static final int NTGenericAll = 0x10000000; + + public static final int NTMaximumAllowed = 0x02000000; + + public static final int NTReadWrite = NTRead + NTWrite; + + /** + * Return the file access mode from the specified flags value. + * + * @param val File flags value. + * @return File access mode. + */ + public static final int getAccessMode(int val) + { + return val & 0x03; + } + + /** + * Return the file sharing mode from the specified flags value. + * + * @param val File flags value. + * @return File sharing mode. + */ + public static final int getSharingMode(int val) + { + return val & 0x70; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/DefaultShareMapper.java b/source/java/org/alfresco/filesys/server/filesys/DefaultShareMapper.java new file mode 100644 index 0000000000..82a70a24b6 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DefaultShareMapper.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.InvalidUserException; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.ShareMapper; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; + +/** + * Default Share Mapper Class + * + *

    Maps disk and print share lookup requests to the list of shares defined in the server + * configuration. + * + * @author GKSpencer + */ +public class DefaultShareMapper implements ShareMapper +{ + // Server configuration + + private ServerConfiguration m_config; + + // Debug enable flag + + private boolean m_debug; + + /** + * Default constructor + */ + public DefaultShareMapper() + { + } + + /** + * Initialize the share mapper + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initializeMapper(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException + { + + // Save the server configuration + + m_config = config; + + // Check if debug is enabled + + if (params != null && params.getChild("debug") != null) + m_debug = true; + } + + /** + * Check if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Find a share using the name and type for the specified client. + * + * @param host String + * @param name String + * @param typ int + * @param sess SrvSession + * @param create boolean + * @return SharedDevice + * @exception InvalidUserException + */ + public SharedDevice findShare(String host, String name, int typ, SrvSession sess, boolean create) + throws InvalidUserException + { + + // Check for the special HOME disk share + + SharedDevice share = null; + + // Find the required share by name/type. Use a case sensitive search first, if that fails + // use a case + // insensitive search. + + share = m_config.getShares().findShare(name, typ, false); + + if (share == null) + { + + // Try a case insensitive search for the required share + + share = m_config.getShares().findShare(name, typ, true); + } + + // Check if the share is available + + if (share != null && share.getContext() != null && share.getContext().isAvailable() == false) + share = null; + + // Return the shared device, or null if no matching device was found + + return share; + } + + /** + * Delete temporary shares for the specified session + * + * @param sess SrvSession + */ + public void deleteShares(SrvSession sess) + { + } + + /** + * Return the list of available shares. + * + * @param host String + * @param sess SrvSession + * @param allShares boolean + * @return SharedDeviceList + */ + public SharedDeviceList getShareList(String host, SrvSession sess, boolean allShares) + { + + // Check if the session is valid, if so then check if the session has any dynamic shares + + // Make a copy of the global share list and add the per session dynamic shares + + SharedDeviceList shrList = new SharedDeviceList(m_config.getShares()); + + // Remove unavailable shares from the list and return the list + + if (allShares == false) + shrList.removeUnavailableShares(); + return shrList; + } + + /** + * Close the share mapper, release any resources. + */ + public void closeMapper() + { + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/DeviceAttribute.java b/source/java/org/alfresco/filesys/server/filesys/DeviceAttribute.java new file mode 100644 index 0000000000..d41095bf24 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DeviceAttribute.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * Device Attribute Constants Class + *

    + * Specifies the constants that can be used to set the DiskDeviceContext device attributes. + */ +public final class DeviceAttribute +{ + // Device attributes + + public static final int Removable = 0x0001; + public static final int ReadOnly = 0x0002; + public static final int FloppyDisk = 0x0004; + public static final int WriteOnce = 0x0008; + public static final int Remote = 0x0010; + public static final int Mounted = 0x0020; + public static final int Virtual = 0x0040; +} diff --git a/source/java/org/alfresco/filesys/server/filesys/DirectoryNotEmptyException.java b/source/java/org/alfresco/filesys/server/filesys/DirectoryNotEmptyException.java new file mode 100644 index 0000000000..2e4af8e208 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DirectoryNotEmptyException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.IOException; + +/** + *

    + * Thrown when an attempt is made to delete a directory that contains files or directories. + */ +public class DirectoryNotEmptyException extends IOException +{ + private static final long serialVersionUID = 3906083464527491128L; + + /** + * Default constructor + */ + public DirectoryNotEmptyException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public DirectoryNotEmptyException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskDeviceContext.java b/source/java/org/alfresco/filesys/server/filesys/DiskDeviceContext.java new file mode 100644 index 0000000000..6bd50144dc --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskDeviceContext.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.server.core.DeviceContext; +import org.alfresco.filesys.server.core.DeviceContextException; +import org.alfresco.filesys.smb.server.notify.NotifyChangeHandler; +import org.alfresco.filesys.smb.server.notify.NotifyRequest; + +/** + * Disk Device Context Class + */ +public class DiskDeviceContext extends DeviceContext +{ + + // Change notification handler + + private NotifyChangeHandler m_changeHandler; + + // Volume information + + private VolumeInfo m_volumeInfo; + + // Disk sizing information + + private SrvDiskInfo m_diskInfo; + + // Filesystem attributes, required to enable features such as compression and encryption + + private int m_filesysAttribs; + + // Disk device attributes, can be used to make the device appear as a removeable, read-only, + // or write-once device for example. + + private int m_deviceAttribs; + + /** + * Class constructor + */ + public DiskDeviceContext() + { + super(); + } + + /** + * Class constructor + * + * @param devName String + */ + public DiskDeviceContext(String devName) + { + super(devName); + } + + /** + * Determine if the volume information is valid + * + * @return boolean + */ + public final boolean hasVolumeInformation() + { + return m_volumeInfo != null ? true : false; + } + + /** + * Return the volume information + * + * @return VolumeInfo + */ + public final VolumeInfo getVolumeInformation() + { + return m_volumeInfo; + } + + /** + * Determine if the disk sizing information is valid + * + * @return boolean + */ + public final boolean hasDiskInformation() + { + return m_diskInfo != null ? true : false; + } + + /** + * Return the disk sizing information + * + * @return SMBSrvDiskInfo + */ + public final SrvDiskInfo getDiskInformation() + { + return m_diskInfo; + } + + /** + * Return the filesystem attributes + * + * @return int + */ + public final int getFilesystemAttributes() + { + return m_filesysAttribs; + } + + /** + * Return the filesystem type, either FileSystem.TypeFAT or FileSystem.TypeNTFS. + * + * Defaults to FileSystem.FAT but will be overridden if the filesystem driver implements the + * NTFSStreamsInterface. + * + * @return String + */ + public String getFilesystemType() + { + return FileSystem.TypeFAT; + } + + /** + * Return the device attributes + * + * @return int + */ + public final int getDeviceAttributes() + { + return m_deviceAttribs; + } + + /** + * Determine if the filesystem is case sensitive or not + * + * @return boolean + */ + public final boolean isCaseless() + { + return (m_filesysAttribs & FileSystem.CasePreservedNames) == 0 ? true : false; + } + + /** + * Enable/disable the change notification handler for this device + * + * @param ena boolean + */ + public final void enableChangeHandler(boolean ena) + { + if (ena == true) + m_changeHandler = new NotifyChangeHandler(this); + else + { + + // Shutdown the change handler, if valid + + if (m_changeHandler != null) + m_changeHandler.shutdownRequest(); + m_changeHandler = null; + } + } + + /** + * Close the disk device context. Release the file state cache resources. + */ + public void CloseContext() + { + + // Call the base class + + super.CloseContext(); + } + + /** + * Determine if the disk context has a change notification handler + * + * @return boolean + */ + public final boolean hasChangeHandler() + { + return m_changeHandler != null ? true : false; + } + + /** + * Return the change notification handler + * + * @return NotifyChangeHandler + */ + public final NotifyChangeHandler getChangeHandler() + { + return m_changeHandler; + } + + /** + * Add a request to the change notification list + * + * @param req NotifyRequest + */ + public final void addNotifyRequest(NotifyRequest req) + { + m_changeHandler.addNotifyRequest(req); + } + + /** + * Remove a request from the notify change request list + * + * @param req NotifyRequest + */ + public final void removeNotifyRequest(NotifyRequest req) + { + m_changeHandler.removeNotifyRequest(req); + } + + /** + * Set the volume information + * + * @param vol VolumeInfo + */ + public final void setVolumeInformation(VolumeInfo vol) + { + m_volumeInfo = vol; + } + + /** + * Set the disk information + * + * @param disk SMBSrvDiskInfo + */ + public final void setDiskInformation(SrvDiskInfo disk) + { + m_diskInfo = disk; + } + + /** + * Set the filesystem attributes + * + * @param attrib int + */ + public final void setFilesystemAttributes(int attrib) + { + m_filesysAttribs = attrib; + } + + /** + * Set the device attributes + * + * @param attrib int + */ + public final void setDeviceAttributes(int attrib) + { + m_deviceAttribs = attrib; + } + + /** + * Context has been initialized and attached to a shared device, do any startup processing in + * this method. + * + * @param share DiskSharedDevice + * @exception DeviceContextException + */ + public void startFilesystem(DiskSharedDevice share) throws DeviceContextException + { + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskFullException.java b/source/java/org/alfresco/filesys/server/filesys/DiskFullException.java new file mode 100644 index 0000000000..e2b510b936 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskFullException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.IOException; + +/** + *

    + * Thrown when a disk write or file extend will exceed the available disk quota for the shared + * filesystem. + */ +public class DiskFullException extends IOException +{ + private static final long serialVersionUID = 3256446901959472181L; + + /** + * Default constructor + */ + public DiskFullException() + { + super(); + } + + /** + * Class constructor + * + * @param msg String + */ + public DiskFullException(String msg) + { + super(msg); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskInfo.java b/source/java/org/alfresco/filesys/server/filesys/DiskInfo.java new file mode 100644 index 0000000000..2aa7781664 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskInfo.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.smb.PCShare; + +/** + * SMB disk information class. + *

    + * The DiskInfo class contains the details of a remote disk share. + */ +public class DiskInfo +{ + + // Node/share details + + protected String m_nodename; + protected String m_share; + + // Total number of allocation units, available allocation units + + protected long m_totalunits; + protected long m_freeunits; + + // Blocks per allocation unit and block size in bytes + + protected long m_blockperunit; + protected long m_blocksize; + + /** + * Construct a blank disk information object. + */ + public DiskInfo() + { + } + + /** + * Class constructor + * + * @param shr PCShare + * @param totunits int + * @param blkunit int + * @param blksiz int + * @param freeunit int + */ + public DiskInfo(PCShare shr, int totunits, int blkunit, int blksiz, int freeunit) + { + if (shr != null) + { + m_nodename = shr.getNodeName(); + m_share = shr.getShareName(); + } + + m_totalunits = (long) totunits; + m_freeunits = (long) freeunit; + + m_blockperunit = (long) blkunit; + m_blocksize = (long) blksiz; + } + + /** + * Class constructor + * + * @param shr PCShare + * @param totunits long + * @param blkunit int + * @param blksiz int + * @param freeunit long + */ + public DiskInfo(PCShare shr, long totunits, int blkunit, int blksiz, long freeunit) + { + if (shr != null) + { + m_nodename = shr.getNodeName(); + m_share = shr.getShareName(); + } + + m_totalunits = totunits; + m_freeunits = freeunit; + + m_blockperunit = (long) blkunit; + m_blocksize = (long) blksiz; + } + + /** + * Get the block size, in bytes. + * + * @return Block size in bytes. + */ + public final int getBlockSize() + { + return (int) m_blocksize; + } + + /** + * Get the number of blocks per allocation unit. + * + * @return Number of blocks per allocation unit. + */ + public final int getBlocksPerAllocationUnit() + { + return (int) m_blockperunit; + } + + /** + * Get the disk free space in kilobytes. + * + * @return Remote disk free space in kilobytes. + */ + public final long getDiskFreeSizeKb() + { + return (((m_freeunits * m_blockperunit) * m_blocksize) / 1024L); + } + + /** + * Get the disk free space in megabytes. + * + * @return Remote disk free space in megabytes. + */ + public final long getDiskFreeSizeMb() + { + return getDiskFreeSizeKb() / 1024L; + } + + /** + * Get the disk size in kilobytes. + * + * @return Remote disk size in kilobytes. + */ + public final long getDiskSizeKb() + { + return (((m_totalunits * m_blockperunit) * m_blocksize) / 1024L); + } + + /** + * Get the disk size in megabytes. + * + * @return Remote disk size in megabytes. + */ + public final long getDiskSizeMb() + { + return (getDiskSizeKb() / 1024L); + } + + /** + * Get the number of free units on this share. + * + * @return Number of free units. + */ + public final long getFreeUnits() + { + return m_freeunits; + } + + /** + * Return the unit size in bytes + * + * @return long + */ + public final long getUnitSize() + { + return m_blockperunit * m_blocksize; + } + + /** + * Get the node name. + * + * @return Node name of the remote server. + */ + public final String getNodeName() + { + return m_nodename; + } + + /** + * Get the share name. + * + * @return Remote share name. + */ + public final String getShareName() + { + return m_share; + } + + /** + * Get the total number of allocation units. + * + * @return The total number of allocation units. + */ + public final long getTotalUnits() + { + return m_totalunits; + } + + /** + * Return the disk information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getTotalUnits()); + str.append("/"); + str.append(getFreeUnits()); + str.append(","); + str.append(getBlockSize()); + str.append("/"); + str.append(getBlocksPerAllocationUnit()); + + str.append(","); + str.append(getDiskSizeMb()); + str.append("Mb/"); + str.append(getDiskFreeSizeMb()); + str.append("Mb"); + + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskInterface.java b/source/java/org/alfresco/filesys/server/filesys/DiskInterface.java new file mode 100644 index 0000000000..30247d3ed1 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskInterface.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.core.DeviceContext; +import org.alfresco.filesys.server.core.DeviceInterface; + +/** + * The disk interface is implemented by classes that provide an interface for a disk type shared + * device. + */ +public interface DiskInterface extends DeviceInterface +{ + + /** + * Close the file. + * + * @param sess Server session + * @param tree Tree connection. + * @param param Network file context. + * @exception java.io.IOException If an error occurs. + */ + public void closeFile(SrvSession sess, TreeConnection tree, NetworkFile param) throws java.io.IOException; + + /** + * Create a new directory on this file system. + * + * @param sess Server session + * @param tree Tree connection. + * @param params Directory create parameters + * @exception java.io.IOException If an error occurs. + */ + public void createDirectory(SrvSession sess, TreeConnection tree, FileOpenParams params) throws java.io.IOException; + + /** + * Create a new file on the file system. + * + * @param sess Server session + * @param tree Tree connection + * @param params File create parameters + * @return NetworkFile + * @exception java.io.IOException If an error occurs. + */ + public NetworkFile createFile(SrvSession sess, TreeConnection tree, FileOpenParams params) + throws java.io.IOException; + + /** + * Delete the directory from the filesystem. + * + * @param sess Server session + * @param tree Tree connection + * @param dir Directory name. + * @exception java.io.IOException The exception description. + */ + public void deleteDirectory(SrvSession sess, TreeConnection tree, String dir) throws java.io.IOException; + + /** + * Delete the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param file NetworkFile + * @exception java.io.IOException The exception description. + */ + public void deleteFile(SrvSession sess, TreeConnection tree, String name) throws java.io.IOException; + + /** + * Check if the specified file exists, and whether it is a file or directory. + * + * @param sess Server session + * @param tree Tree connection + * @param name java.lang.String + * @return int + * @see FileStatus + */ + int fileExists(SrvSession sess, TreeConnection tree, String name); + + /** + * Flush any buffered output for the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file context. + * @exception java.io.IOException The exception description. + */ + public void flushFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws java.io.IOException; + + /** + * Get the file information for the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param name File name/path that information is required for. + * @return File information if valid, else null + * @exception java.io.IOException The exception description. + */ + public FileInfo getFileInformation(SrvSession sess, TreeConnection tree, String name) throws java.io.IOException; + + /** + * Determine if the disk device is read-only. + * + * @param sess Server session + * @param ctx Device context + * @return boolean + * @exception java.io.IOException If an error occurs. + */ + boolean isReadOnly(SrvSession sess, DeviceContext ctx) throws java.io.IOException; + + /** + * Open a file on the file system. + * + * @param sess Server session + * @param tree Tree connection + * @param params File open parameters + * @return NetworkFile + * @exception java.io.IOException If an error occurs. + */ + public NetworkFile openFile(SrvSession sess, TreeConnection tree, FileOpenParams params) throws java.io.IOException; + + /** + * Read a block of data from the specified file. + * + * @param sess Session details + * @param tree Tree connection + * @param file Network file + * @param buf Buffer to return data to + * @param bufPos Starting position in the return buffer + * @param siz Maximum size of data to return + * @param filePos File offset to read data + * @return Number of bytes read + * @exception java.io.IOException The exception description. + */ + public int readFile(SrvSession sess, TreeConnection tree, NetworkFile file, byte[] buf, int bufPos, int siz, + long filePos) throws java.io.IOException; + + /** + * Rename the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param oldName java.lang.String + * @param newName java.lang.String + * @exception java.io.IOException The exception description. + */ + public void renameFile(SrvSession sess, TreeConnection tree, String oldName, String newName) + throws java.io.IOException; + + /** + * Seek to the specified file position. + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file. + * @param pos Position to seek to. + * @param typ Seek type. + * @return New file position, relative to the start of file. + */ + long seekFile(SrvSession sess, TreeConnection tree, NetworkFile file, long pos, int typ) throws java.io.IOException; + + /** + * Set the file information for the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param name java.lang.String + * @param info FileInfo + * @exception java.io.IOException The exception description. + */ + public void setFileInformation(SrvSession sess, TreeConnection tree, String name, FileInfo info) + throws java.io.IOException; + + /** + * Start a new search on the filesystem using the specified searchPath that may contain + * wildcards. + * + * @param sess Server session + * @param tree Tree connection + * @param searchPath File(s) to search for, may include wildcards. + * @param attrib Attributes of the file(s) to search for, see class SMBFileAttribute. + * @return SearchContext + * @exception java.io.FileNotFoundException If the search could not be started. + */ + public SearchContext startSearch(SrvSession sess, TreeConnection tree, String searchPath, int attrib) + throws java.io.FileNotFoundException; + + /** + * Truncate a file to the specified size + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file details + * @param siz New file length + * @exception java.io.IOException The exception description. + */ + public void truncateFile(SrvSession sess, TreeConnection tree, NetworkFile file, long siz) + throws java.io.IOException; + + /** + * Write a block of data to the file. + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file details + * @param buf byte[] Data to be written + * @param bufoff Offset within the buffer that the data starts + * @param siz int Data length + * @param fileoff Position within the file that the data is to be written. + * @return Number of bytes actually written + * @exception java.io.IOException The exception description. + */ + public int writeFile(SrvSession sess, TreeConnection tree, NetworkFile file, byte[] buf, int bufoff, int siz, + long fileoff) throws java.io.IOException; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskSharedDevice.java b/source/java/org/alfresco/filesys/server/filesys/DiskSharedDevice.java new file mode 100644 index 0000000000..9424de4eb1 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskSharedDevice.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.server.core.DeviceInterface; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + *

    + * A disk shared device has a name, a driver class and a context for the driver. + */ +public class DiskSharedDevice extends SharedDevice +{ + + /** + * Construct a disk share with the specified name and device interface. + * + * @param name Disk share name. + * @param iface Disk device interface. + * @param ctx Context that will be passed to the device interface. + */ + public DiskSharedDevice(String name, DeviceInterface iface, DiskDeviceContext ctx) + { + super(name, ShareType.DISK, ctx); + setInterface(iface); + } + + /** + * Construct a disk share with the specified name and device interface. + * + * @param name java.lang.String + * @param iface DeviceInterface + * @param ctx DeviceContext + * @param attrib int + */ + public DiskSharedDevice(String name, DeviceInterface iface, DiskDeviceContext ctx, int attrib) + { + super(name, ShareType.DISK, ctx); + setInterface(iface); + setAttributes(attrib); + } + + /** + * Return the disk device context + * + * @return DiskDeviceContext + */ + public final DiskDeviceContext getDiskContext() + { + return (DiskDeviceContext) getContext(); + } + + /** + * Return the disk interface + * + * @return DiskInterface + */ + public final DiskInterface getDiskInterface() + { + try + { + if (getInterface() instanceof DiskInterface) + return (DiskInterface) getInterface(); + } + catch (Exception ex) + { + } + return null; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskSizeInterface.java b/source/java/org/alfresco/filesys/server/filesys/DiskSizeInterface.java new file mode 100644 index 0000000000..e7acada7f4 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskSizeInterface.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * Disk Size Interface + *

    + * Optional interface that a DiskInterface driver can implement to provide disk sizing information. + * The disk size information may also be specified via the configuration. + */ +public interface DiskSizeInterface +{ + + /** + * Get the disk information for this shared disk device. + * + * @param cts DiskDeviceContext + * @param diskDev SrvDiskInfo + * @exception java.io.IOException The exception description. + */ + public void getDiskInformation(DiskDeviceContext ctx, SrvDiskInfo diskDev) throws java.io.IOException; +} diff --git a/source/java/org/alfresco/filesys/server/filesys/DiskVolumeInterface.java b/source/java/org/alfresco/filesys/server/filesys/DiskVolumeInterface.java new file mode 100644 index 0000000000..34956c9fc1 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/DiskVolumeInterface.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * Disk Volume Interface + *

    + * Optional interface that a DiskInterface driver can implement to provide disk volume information. + * The disk volume information may also be specified via the configuration. + */ +public interface DiskVolumeInterface +{ + + /** + * Return the disk device volume information. + * + * @param ctx DiskDeviceContext + * @return VolumeInfo + */ + public VolumeInfo getVolumeInformation(DiskDeviceContext ctx); +} diff --git a/source/java/org/alfresco/filesys/server/filesys/FileAccess.java b/source/java/org/alfresco/filesys/server/filesys/FileAccess.java new file mode 100644 index 0000000000..57b94bdc15 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileAccess.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * File Access Class + *

    + * Contains a list of the available file permissions that may be applied to a share, directory or + * file. + */ +public final class FileAccess +{ + // Permissions + + public static final int NoAccess = 0; + public static final int ReadOnly = 1; + public static final int Writeable = 2; + + /** + * Return the file permission as a string. + * + * @param perm int + * @return java.lang.String + */ + public final static String asString(int perm) + { + String permStr = ""; + + switch (perm) + { + case NoAccess: + permStr = "NoAccess"; + break; + case ReadOnly: + permStr = "ReadOnly"; + break; + case Writeable: + permStr = "Writeable"; + break; + } + return permStr; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/FileAction.java b/source/java/org/alfresco/filesys/server/filesys/FileAction.java new file mode 100644 index 0000000000..091a61e906 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileAction.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * The file actions are sent in OpenAndX and NTCreateAndX request/response SMBs. + */ +public final class FileAction +{ + // File open action request codes + + public static final int FailIfExists = 0x0000; + public static final int OpenIfExists = 0x0001; + public static final int TruncateExisting = 0x0002; + public static final int CreateNotExist = 0x0010; + + // File open action response codes + + public static final int FileExisted = 0x0001; + public static final int FileCreated = 0x0002; + public static final int FileTruncated = 0x0003; + + // NT file/device open action codes + + public final static int NTSupersede = 0; // supersede if exists, else create a new file + public final static int NTOpen = 1; // only open if the file exists + public final static int NTCreate = 2; // create if file does not exist, else fail + public final static int NTOpenIf = 3; // open if exists else create + public final static int NTOverwrite = 4; // overwrite if exists, else fail + public final static int NTOverwriteIf = 5; // overwrite if exists, else create + + /** + * Check if the file action value indicates that the file should be created if the file does not + * exist. + * + * @return boolean + * @param action int + */ + public final static boolean createNotExists(int action) + { + if ((action & CreateNotExist) != 0) + return true; + return false; + } + + /** + * Check if the open file if exists action is set. + * + * @return boolean + * @param action int + */ + public final static boolean openIfExists(int action) + { + if ((action & OpenIfExists) != 0) + return true; + return false; + } + + /** + * Check if the existing file should be truncated. + * + * @return boolean + * @param action int + */ + public final static boolean truncateExistingFile(int action) + { + if ((action & TruncateExisting) != 0) + return true; + return false; + } + + /** + * Convert the file exists action flags to a string + * + * @param flags int + * @return String + */ + public final static String asString(int flags) + { + StringBuffer str = new StringBuffer(); + + str.append("[0x"); + str.append(Integer.toHexString(flags)); + str.append(":"); + + if (openIfExists(flags)) + str.append("OpenExists|"); + + if (truncateExistingFile(flags)) + str.append("Truncate|"); + + if (createNotExists(flags)) + str.append("CreateNotExist"); + + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/FileAttribute.java b/source/java/org/alfresco/filesys/server/filesys/FileAttribute.java new file mode 100644 index 0000000000..9db10c88ac --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileAttribute.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * SMB file attribute class. + *

    + * Defines various bit masks that may be returned in an FileInfo object, that is returned by the + * DiskInterface.getFileInformation () and SearchContext.nextFileInfo() methods. + *

    + * The values are also used by the DiskInterface.StartSearch () method to determine the + * file/directory types that are returned. + * + * @see DiskInterface + * @see SearchContext + */ +public final class FileAttribute +{ + + // Standard file attribute constants + + public static final int Normal = 0x00; + public static final int ReadOnly = 0x01; + public static final int Hidden = 0x02; + public static final int System = 0x04; + public static final int Volume = 0x08; + public static final int Directory = 0x10; + public static final int Archive = 0x20; + + // NT file attribute flags + + public static final int NTReadOnly = 0x00000001; + public static final int NTHidden = 0x00000002; + public static final int NTSystem = 0x00000004; + public static final int NTVolumeId = 0x00000008; + public static final int NTDirectory = 0x00000010; + public static final int NTArchive = 0x00000020; + public static final int NTDevice = 0x00000040; + public static final int NTNormal = 0x00000080; + public static final int NTTemporary = 0x00000100; + public static final int NTSparse = 0x00000200; + public static final int NTReparsePoint = 0x00000400; + public static final int NTCompressed = 0x00000800; + public static final int NTOffline = 0x00001000; + public static final int NTIndexed = 0x00002000; + public static final int NTEncrypted = 0x00004000; + public static final int NTOpenNoRecall = 0x00100000; + public static final int NTOpenReparsePoint = 0x00200000; + public static final int NTPosixSemantics = 0x01000000; + public static final int NTBackupSemantics = 0x02000000; + public static final int NTDeleteOnClose = 0x04000000; + public static final int NTSequentialScan = 0x08000000; + public static final int NTRandomAccess = 0x10000000; + public static final int NTNoBuffering = 0x20000000; + public static final int NTOverlapped = 0x40000000; + public static final int NTWriteThrough = 0x80000000; + + /** + * Determine if the specified file attribute mask has the specified file attribute enabled. + * + * @return boolean + * @param attr int + * @param reqattr int + */ + public final static boolean hasAttribute(int attr, int reqattr) + { + + // Check for the specified attribute + + if ((attr & reqattr) != 0) + return true; + return false; + } + + /** + * Check if the read-only attribute is set + * + * @param attr int + * @return boolean + */ + public static final boolean isReadOnly(int attr) + { + return (attr & ReadOnly) != 0 ? true : false; + } + + /** + * Check if the directory attribute is set + * + * @param attr int + * @return boolean + */ + public static final boolean isDirectory(int attr) + { + return (attr & Directory) != 0 ? true : false; + } + + /** + * Check if the hidden attribute is set + * + * @param attr int + * @return boolean + */ + public static final boolean isHidden(int attr) + { + return (attr & Hidden) != 0 ? true : false; + } + + /** + * Check if the system attribute is set + * + * @param attr int + * @return boolean + */ + public static final boolean isSystem(int attr) + { + return (attr & System) != 0 ? true : false; + } + + /** + * Check if the archive attribute is set + * + * @param attr int + * @return boolean + */ + public static final boolean isArchived(int attr) + { + return (attr & Archive) != 0 ? true : false; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/FileExistsException.java b/source/java/org/alfresco/filesys/server/filesys/FileExistsException.java new file mode 100644 index 0000000000..3ac2ae1df8 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileExistsException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * This exception may be thrown by a disk interface when an attempt to create a new file fails + * because the file already exists. + */ +public class FileExistsException extends java.io.IOException +{ + private static final long serialVersionUID = 3258408439242895670L; + + /** + * FileExistsException constructor. + */ + public FileExistsException() + { + super(); + } + + /** + * FileExistsException constructor. + * + * @param s java.lang.String + */ + public FileExistsException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/FileIdInterface.java b/source/java/org/alfresco/filesys/server/filesys/FileIdInterface.java new file mode 100644 index 0000000000..37df123bf5 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileIdInterface.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.FileNotFoundException; + +import org.alfresco.filesys.server.SrvSession; + +/** + * File Id Interface + *

    + * Optional interface that a DiskInterface driver can implement to provide file id to path + * conversion. + */ +public interface FileIdInterface +{ + + /** + * Convert a file id to a share relative path + * + * @param sess SrvSession + * @param tree TreeConnection + * @param dirid int + * @param fileid + * @return String + * @exception FileNotFoundException + */ + public String buildPathForFileId(SrvSession sess, TreeConnection tree, int dirid, int fileid) + throws FileNotFoundException; +} diff --git a/source/java/org/alfresco/filesys/server/filesys/FileInfo.java b/source/java/org/alfresco/filesys/server/filesys/FileInfo.java new file mode 100644 index 0000000000..331b13cd76 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileInfo.java @@ -0,0 +1,946 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.Serializable; +import java.util.Date; + +import org.alfresco.filesys.smb.SMBDate; + +/** + * File information class. + *

    + * The FileInfo class is returned by the DiskInterface.getFileInformation () and + * SearchContext.nextFileInfo() methods. + * + * @see DiskInterface + * @see SearchContext + */ +public class FileInfo implements Serializable +{ + private static final long serialVersionUID = 5710753560656277110L; + + // Constants + // + // Set file information flags + + public static final int SetFileSize = 0x0001; + public static final int SetAllocationSize = 0x0002; + public static final int SetAttributes = 0x0004; + public static final int SetModifyDate = 0x0008; + public static final int SetCreationDate = 0x0010; + public static final int SetAccessDate = 0x0020; + public static final int SetChangeDate = 0x0040; + public static final int SetGid = 0x0080; + public static final int SetUid = 0x0100; + public static final int SetMode = 0x0200; + public static final int SetDeleteOnClose = 0x0400; + + // File name string + + protected String m_name; + + // 8.3 format file name + + protected String m_shortName; + + // Path string + + protected String m_path; + + // File size, in bytes + + protected long m_size; + + // File attributes bits + + protected int m_attr = -1; + + // File modification date/time + + private long m_modifyDate; + + // Creation date/time + + private long m_createDate; + + // Last access date/time (if available) + + private long m_accessDate; + + // Change date/time (for Un*x inode changes) + + private long m_changeDate; + + // Filesystem allocation size + + private long m_allocSize; + + // File identifier and parent directory id + + private int m_fileId = -1; + private int m_dirId = -1; + + // User/group id + + private int m_gid = -1; + private int m_uid = -1; + + // Unix mode + + private int m_mode = -1; + + // Delete file on close + + private boolean m_deleteOnClose; + + // Set file information flags + // + // Used to indicate which values in the file information object are valid and should be used to + // set + // the file information. + + private int m_setFlags; + + /** + * Default constructor + */ + public FileInfo() + { + } + + /** + * Construct an SMB file information object. + * + * @param fname File name string. + * @param fsize File size, in bytes. + * @param fattr File attributes. + */ + public FileInfo(String fname, long fsize, int fattr) + { + m_name = fname; + m_size = fsize; + m_attr = fattr; + + setAllocationSize(0); + } + + /** + * Construct an SMB file information object. + * + * @param fname File name string. + * @param fsize File size, in bytes. + * @param fattr File attributes. + * @param ftime File time, in seconds since 1-Jan-1970 00:00:00 + */ + public FileInfo(String fname, long fsize, int fattr, int ftime) + { + m_name = fname; + m_size = fsize; + m_attr = fattr; + m_modifyDate = new SMBDate(ftime).getTime(); + + setAllocationSize(0); + } + + /** + * Construct an SMB file information object. + * + * @param fname File name string. + * @param fsize File size, in bytes. + * @param fattr File attributes. + * @param fdate SMB encoded file date. + * @param ftime SMB encoded file time. + */ + public FileInfo(String fname, long fsize, int fattr, int fdate, int ftime) + { + m_name = fname; + m_size = fsize; + m_attr = fattr; + + if (fdate != 0 && ftime != 0) + m_modifyDate = new SMBDate(fdate, ftime).getTime(); + + setAllocationSize(0); + } + + /** + * Construct an SMB file information object. + * + * @param fpath File path string. + * @param fname File name string. + * @param fsize File size, in bytes. + * @param fattr File attributes. + */ + public FileInfo(String fpath, String fname, long fsize, int fattr) + { + m_path = fpath; + m_name = fname; + m_size = fsize; + m_attr = fattr; + + setAllocationSize(0); + } + + /** + * Construct an SMB file information object. + * + * @param fpath File path string. + * @param fname File name string. + * @param fsize File size, in bytes. + * @param fattr File attributes. + * @param ftime File time, in seconds since 1-Jan-1970 00:00:00 + */ + public FileInfo(String fpath, String fname, long fsize, int fattr, int ftime) + { + m_path = fpath; + m_name = fname; + m_size = fsize; + m_attr = fattr; + m_modifyDate = new SMBDate(ftime).getTime(); + + setAllocationSize(0); + } + + /** + * Construct an SMB file information object. + * + * @param fpath File path string. + * @param fname File name string. + * @param fsize File size, in bytes. + * @param fattr File attributes. + * @param fdate SMB encoded file date. + * @param ftime SMB encoded file time. + */ + public FileInfo(String fpath, String fname, long fsize, int fattr, int fdate, int ftime) + { + m_path = fpath; + m_name = fname; + m_size = fsize; + m_attr = fattr; + m_modifyDate = new SMBDate(fdate, ftime).getTime(); + + setAllocationSize(0); + } + + /** + * Return the files last access date/time. + * + * @return long + */ + public long getAccessDateTime() + { + return m_accessDate; + } + + /** + * Get the files allocated size. + * + * @return long + */ + public long getAllocationSize() + { + return m_allocSize; + } + + /** + * Get the files allocated size, as a 32bit value + * + * @return int + */ + public int getAllocationSizeInt() + { + return (int) (m_allocSize & 0x0FFFFFFFFL); + } + + /** + * Return the inode change date/time of the file. + * + * @return long + */ + public long getChangeDateTime() + { + return m_changeDate; + } + + /** + * Return the creation date/time of the file. + * + * @return long + */ + public long getCreationDateTime() + { + return m_createDate; + } + + /** + * Return the delete on close flag setting + * + * @return boolean + */ + public final boolean hasDeleteOnClose() + { + return m_deleteOnClose; + } + + /** + * Return the file attributes value. + * + * @return File attributes value. + */ + public int getFileAttributes() + { + return m_attr; + } + + /** + * Get the file name string + * + * @return File name string. + */ + public final String getFileName() + { + return m_name; + } + + /** + * Check if the short (8.3) file name is available + * + * @return boolean + */ + public final boolean hasShortName() + { + return m_shortName != null ? true : false; + } + + /** + * Get the short file name (8.3 format) + * + * @return String + */ + public final String getShortName() + { + return m_shortName; + } + + /** + * Get the files date/time of last write + * + * @return long + */ + public final long getModifyDateTime() + { + return m_modifyDate; + } + + /** + * Get the file path string. + * + * @return File path string, relative to the share. + */ + public final String getPath() + { + return m_path; + } + + /** + * Get the file size, in bytes. + * + * @return File size in bytes. + */ + public final long getSize() + { + return m_size; + } + + /** + * Get the file size in bytes, as a 32bit value + * + * @return File size in bytes, as an int + */ + public final int getSizeInt() + { + return (int) (m_size & 0x0FFFFFFFFL); + } + + /** + * Get the file identifier + * + * @return int + */ + public final int getFileId() + { + return m_fileId; + } + + /** + * Get the file identifier + * + * @return long + */ + public final long getFileIdLong() + { + return ((long) m_fileId) & 0xFFFFFFFFL; + } + + /** + * Get the parent directory identifier + * + * @return int + */ + public final int getDirectoryId() + { + return m_dirId; + } + + /** + * Get the parent directory identifier + * + * @return long + */ + public final long getDirectoryIdLong() + { + return ((long) m_dirId) & 0xFFFFFFFFL; + } + + /** + * Determine if the last access date/time is available. + * + * @return boolean + */ + public boolean hasAccessDateTime() + { + return m_accessDate == 0L ? false : true; + } + + /** + * Determine if the inode change date/time details are available. + * + * @return boolean + */ + public boolean hasChangeDateTime() + { + return m_changeDate == 0L ? false : true; + } + + /** + * Determine if the creation date/time details are available. + * + * @return boolean + */ + public boolean hasCreationDateTime() + { + return m_createDate == 0L ? false : true; + } + + /** + * Determine if the modify date/time details are available. + * + * @return boolean + */ + public boolean hasModifyDateTime() + { + return m_modifyDate == 0L ? false : true; + } + + /** + * Determine if the file attributes field has been set + * + * @return boolean + */ + public final boolean hasFileAttributes() + { + return m_attr != -1 ? true : false; + } + + /** + * Return the specified attribute status + * + * @param attr int + */ + public final boolean hasAttribute(int attr) + { + return (m_attr & attr) != 0 ? true : false; + } + + /** + * Return the directory file attribute status. + * + * @return true if the file is a directory, else false. + */ + public final boolean isDirectory() + { + return (m_attr & FileAttribute.Directory) != 0 ? true : false; + } + + /** + * Return the hidden file attribute status. + * + * @return true if the file is hidden, else false. + */ + public final boolean isHidden() + { + return (m_attr & FileAttribute.Hidden) != 0 ? true : false; + } + + /** + * Return the read-only file attribute status. + * + * @return true if the file is read-only, else false. + */ + public final boolean isReadOnly() + { + return (m_attr & FileAttribute.ReadOnly) != 0 ? true : false; + } + + /** + * Return the system file attribute status. + * + * @return true if the file is a system file, else false. + */ + public final boolean isSystem() + { + return (m_attr & FileAttribute.System) != 0 ? true : false; + } + + /** + * Return the archived attribute status + * + * @return boolean + */ + public final boolean isArchived() + { + return (m_attr & FileAttribute.Archive) != 0 ? true : false; + } + + /** + * Determine if the group id field has been set + * + * @return boolean + */ + public final boolean hasGid() + { + return m_gid != -1 ? true : false; + } + + /** + * Return the owner group id + * + * @return int + */ + public final int getGid() + { + return m_gid; + } + + /** + * Determine if the user id field has been set + * + * @return boolean + */ + public final boolean hasUid() + { + return m_uid != -1 ? true : false; + } + + /** + * Return the owner user id + * + * @return int + */ + public final int getUid() + { + return m_uid; + } + + /** + * Determine if the mode field has been set + * + * @return boolean + */ + public final boolean hasMode() + { + return m_mode != -1 ? true : false; + } + + /** + * Return the Unix mode + * + * @return int + */ + public final int getMode() + { + return m_mode; + } + + /** + * Reset all values to zero/null values. + */ + public final void resetInfo() + { + m_name = ""; + m_path = null; + + m_size = 0L; + m_allocSize = 0L; + + m_attr = 0; + + m_accessDate = 0L; + m_createDate = 0L; + m_modifyDate = 0L; + m_changeDate = 0L; + + m_fileId = -1; + m_dirId = -1; + + m_gid = -1; + m_uid = -1; + m_mode = -1; + } + + /** + * Copy the file information + * + * @param finfo FileInfo + */ + public final void copyFrom(FileInfo finfo) + { + m_name = finfo.getFileName(); + m_path = finfo.getPath(); + + m_size = finfo.getSize(); + m_allocSize = finfo.getAllocationSize(); + + m_attr = finfo.getFileAttributes(); + + m_accessDate = finfo.getAccessDateTime(); + m_createDate = finfo.getCreationDateTime(); + m_modifyDate = finfo.getModifyDateTime(); + m_changeDate = finfo.getChangeDateTime(); + + m_fileId = finfo.getFileId(); + m_dirId = finfo.getDirectoryId(); + + m_gid = finfo.getGid(); + m_uid = finfo.getUid(); + m_mode = finfo.getMode(); + } + + /** + * Set the files last access date/time. + * + * @param timesec long + */ + public void setAccessDateTime(long timesec) + { + + // Create the access date/time + + m_accessDate = timesec; + } + + /** + * Set the files allocation size. + * + * @param siz long + */ + public void setAllocationSize(long siz) + { + m_allocSize = siz; + } + + /** + * Set the inode change date/time for the file. + * + * @param timesec long + */ + public void setChangeDateTime(long timesec) + { + + // Set the inode change date/time + + m_changeDate = timesec; + } + + /** + * Set the creation date/time for the file. + * + * @param timesec long + */ + public void setCreationDateTime(long timesec) + { + + // Set the creation date/time + + m_createDate = timesec; + } + + /** + * Set/clear the delete on close flag + * + * @param del boolean + */ + public final void setDeleteOnClose(boolean del) + { + m_deleteOnClose = del; + } + + /** + * Set the file attributes. + * + * @param attr int + */ + public final void setFileAttributes(int attr) + { + m_attr = attr; + } + + /** + * Set the file name. + * + * @param name java.lang.String + */ + public final void setFileName(String name) + { + m_name = name; + } + + /** + * Set the file size in bytes + * + * @param siz long + */ + public final void setFileSize(long siz) + { + m_size = siz; + } + + /** + * Set the modification date/time for the file. + * + * @param timesec long + */ + public void setModifyDateTime(long timesec) + { + + // Set the date/time + + m_modifyDate = timesec; + } + + /** + * Set the file identifier + * + * @param id int + */ + public final void setFileId(int id) + { + m_fileId = id; + } + + /** + * Set the parent directory id + * + * @param id int + */ + public final void setDirectoryId(int id) + { + m_dirId = id; + } + + /** + * Set the short (8.3 format) file name + * + * @param name String + */ + public final void setShortName(String name) + { + m_shortName = name; + } + + /** + * Set the path + * + * @param path String + */ + public final void setPath(String path) + { + m_path = path; + } + + /** + * Set the file size. + * + * @param siz int + */ + public final void setSize(int siz) + { + m_size = siz; + } + + /** + * Set the file size. + * + * @param siz long + */ + public final void setSize(long siz) + { + m_size = siz; + } + + /** + * Set the owner group id + * + * @param id int + */ + public final void setGid(int id) + { + m_gid = id; + } + + /** + * Set the owner user id + * + * @param id int + */ + public final void setUid(int id) + { + m_uid = id; + } + + /** + * Set the file mode + * + * @param mode int + */ + public final void setMode(int mode) + { + m_mode = mode; + } + + /** + * Set the set file information flags to indicated which values are to be set + * + * @param setFlags int + */ + public final void setFileInformationFlags(int setFlags) + { + m_setFlags = setFlags; + } + + /** + * Determine if the specified set file information flags is enabled + * + * @param setFlag int + * @return boolean + */ + public final boolean hasSetFlag(int flag) + { + if ((m_setFlags & flag) != 0) + return true; + return false; + } + + /** + * Return the set file information flags + * + * @return int + */ + public final int getSetFileInformationFlags() + { + return m_setFlags; + } + + /** + * Return the file information as a string. + * + * @return File information string. + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + // Append the path, and terminate with a trailing '\' + + if (m_path != null) + { + str.append(m_path); + if (!m_path.endsWith("\\")) + str.append("\\"); + } + + // Append the file name + + str.append(m_name); + + // Space fill + + while (str.length() < 15) + str.append(" "); + + // Append the attribute states + + if (isReadOnly()) + str.append("R"); + else + str.append("-"); + if (isHidden()) + str.append("H"); + else + str.append("-"); + if (isSystem()) + str.append("S"); + else + str.append("-"); + if (isDirectory()) + str.append("D"); + else + str.append("-"); + + // Append the file size, in bytes + + str.append(" "); + str.append(m_size); + + // Space fill + + while (str.length() < 30) + str.append(" "); + + // Append the file write date/time, if available + + if (m_modifyDate != 0L) + { + str.append(" - "); + str.append(new Date(m_modifyDate)); + } + + // Append the short (8.3) file name, if available + + if (hasShortName()) + { + str.append(" ("); + str.append(getShortName()); + str.append(")"); + } + + // Return the file information string + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/FileName.java b/source/java/org/alfresco/filesys/server/filesys/FileName.java new file mode 100644 index 0000000000..512bdb2cb3 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileName.java @@ -0,0 +1,626 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.StringTokenizer; + +/** + *

    + * Provides utility methods for manipulating file names. + */ +public final class FileName +{ + + // DOS file name seperator + + public static final char DOS_SEPERATOR = '\\'; + public static final String DOS_SEPERATOR_STR = "\\"; + + // NTFS Stream seperator + + public static final String NTFSStreamSeperator = ":"; + + /** + * Build a path using the specified components. + * + * @param dev java.lang.String + * @param path java.lang.String + * @param filename java.lang.String + * @param sep char + * @return java.lang.String + */ + public static String buildPath(String dev, String path, String filename, char sep) + { + + // Debug.println ( "BuildPath: dev=" + dev + ", path=" + path + ",filename=" + filename); + + // Build the path string + + StringBuffer fullPath = new StringBuffer(); + + // Check for a device name + + if (dev != null) + { + + // Add the device name + + fullPath.append(dev); + + // Check if the device name has a file seperator + + if (dev.length() > 0 && dev.charAt(dev.length() - 1) != sep) + fullPath.append(sep); + } + + // Check for a path + + if (path != null) + { + + // Add the path + + if (fullPath.length() > 0 && path.length() > 0 + && (path.charAt(0) == sep || path.charAt(0) == DOS_SEPERATOR)) + fullPath.append(path.substring(1)); + else + fullPath.append(path); + + // Add a trailing seperator, if required + + if (path.length() > 0 && path.charAt(path.length() - 1) != sep && filename != null) + fullPath.append(sep); + } + + // Check for a file name + + if (filename != null) + { + + // Add the file name + + if (fullPath.length() > 0 && filename.length() > 0 + && (filename.charAt(0) == sep || filename.charAt(0) == DOS_SEPERATOR)) + fullPath.append(filename.substring(1)); + else + fullPath.append(filename); + } + + // Debug + + // Debug.println ( "BuildPath: " + fullPath.toString ()); + + // Convert the file seperator characters in the path if we are not using the normal + // DOS file seperator character. + + if (sep != DOS_SEPERATOR) + return convertSeperators(fullPath.toString(), sep); + return fullPath.toString(); + } + + /** + * Convert the file seperators in a path to the specified path seperator character. + * + * @param path java.lang.String + * @param sep char + * @return java.lang.String + */ + public static String convertSeperators(String path, char sep) + { + + // Check if the path contains any DOS seperators + + if (path.indexOf(DOS_SEPERATOR) == -1) + return path; + + // Convert DOS path seperators to the specified seperator + + StringBuffer newPath = new StringBuffer(); + int idx = 0; + + while (idx < path.length()) + { + + // Get the current character from the path and check if it is a DOS path + // seperator character. + + char ch = path.charAt(idx++); + if (ch == DOS_SEPERATOR) + newPath.append(sep); + else + newPath.append(ch); + } + + // Return the new path string + + return newPath.toString(); + } + + /** + * Map the input path to a real path, this may require changing the case of various parts of the + * path. The base path is not checked, it is assumed to exist. + * + * @param base java.lang.String + * @param path java.lang.String + * @return java.lang.String + * @exception java.io.FileNotFoundException The path could not be mapped to a real path. + */ + public static final String mapPath(String base, String path) throws java.io.FileNotFoundException + { + + // Split the path string into seperate directory components + + String pathCopy = path; + if (pathCopy.length() > 0 && pathCopy.startsWith(DOS_SEPERATOR_STR)) + pathCopy = pathCopy.substring(1); + + StringTokenizer token = new StringTokenizer(pathCopy, "\\/"); + int tokCnt = token.countTokens(); + + // The mapped path string, if it can be mapped + + String mappedPath = null; + + if (tokCnt > 0) + { + + // Allocate an array to hold the directory names + + String[] dirs = new String[token.countTokens()]; + + // Get the directory names + + int idx = 0; + while (token.hasMoreTokens()) + dirs[idx++] = token.nextToken(); + + // Check if the path ends with a directory or file name, ie. has a trailing '\' or not + + int maxDir = dirs.length; + + if (path.endsWith(DOS_SEPERATOR_STR) == false) + { + + // Ignore the last token as it is a file name + + maxDir--; + } + + // Build up the path string and validate that the path exists at each stage. + + StringBuffer pathStr = new StringBuffer(base); + if (base.endsWith(java.io.File.separator) == false) + pathStr.append(java.io.File.separator); + + int lastPos = pathStr.length(); + idx = 0; + File lastDir = null; + if (base != null && base.length() > 0) + lastDir = new File(base); + File curDir = null; + + while (idx < maxDir) + { + + // Append the current directory to the path + + pathStr.append(dirs[idx]); + pathStr.append(java.io.File.separator); + + // Check if the current path exists + + curDir = new File(pathStr.toString()); + + if (curDir.exists() == false) + { + + // Check if there is a previous directory to search + + if (lastDir == null) + throw new FileNotFoundException(); + + // Search the current path for a matching directory, the case may be different + + String[] fileList = lastDir.list(); + if (fileList == null || fileList.length == 0) + throw new FileNotFoundException(); + + int fidx = 0; + boolean foundPath = false; + + while (fidx < fileList.length && foundPath == false) + { + + // Check if the current file name matches the required directory name + + if (fileList[fidx].equalsIgnoreCase(dirs[idx])) + { + + // Use the current directory name + + pathStr.setLength(lastPos); + pathStr.append(fileList[fidx]); + pathStr.append(java.io.File.separator); + + // Check if the path is valid + + curDir = new File(pathStr.toString()); + if (curDir.exists()) + { + foundPath = true; + break; + } + } + + // Update the file name index + + fidx++; + } + + // Check if we found the required directory + + if (foundPath == false) + throw new FileNotFoundException(); + } + + // Set the last valid directory file + + lastDir = curDir; + + // Update the end of valid path location + + lastPos = pathStr.length(); + + // Update the current directory index + + idx++; + } + + // Check if there is a file name to be added to the mapped path + + if (path.endsWith(DOS_SEPERATOR_STR) == false) + { + + // Map the file name + + String[] fileList = lastDir.list(); + String fileName = dirs[dirs.length - 1]; + + // Check if the file list is valid, if not then the path is not valid + + if (fileList == null) + throw new FileNotFoundException(path); + + // Search for the required file + + idx = 0; + boolean foundFile = false; + + while (idx < fileList.length && foundFile == false) + { + if (fileList[idx].compareTo(fileName) == 0) + foundFile = true; + else + idx++; + } + + // Check if we found the file name, if not then do a case insensitive search + + if (foundFile == false) + { + + // Search again using a case insensitive search + + idx = 0; + + while (idx < fileList.length && foundFile == false) + { + if (fileList[idx].equalsIgnoreCase(fileName)) + { + foundFile = true; + fileName = fileList[idx]; + } + else + idx++; + } + } + + // Append the file name + + pathStr.append(fileName); + } + + // Set the new path string + + mappedPath = pathStr.toString(); + } + + // Return the mapped path string, if successful. + + return mappedPath; + } + + /** + * Remove the file name from the specified path string. + * + * @param path java.lang.String + * @return java.lang.String + */ + public final static String removeFileName(String path) + { + + // Find the last path seperator + + int pos = path.lastIndexOf(DOS_SEPERATOR); + if (pos != -1) + return path.substring(0, pos); + + // Return an empty string, no path seperators + + return ""; + } + + /** + * Split the path into seperate directory path and file name strings. + * + * @param path Full path string. + * @param sep Path seperator character. + * @return java.lang.String[] + */ + public static String[] splitPath(String path) + { + return splitPath(path, DOS_SEPERATOR, null); + } + + /** + * Split the path into seperate directory path and file name strings. + * + * @param path Full path string. + * @param sep Path seperator character. + * @return java.lang.String[] + */ + public static String[] splitPath(String path, char sep) + { + return splitPath(path, sep, null); + } + + /** + * Split the path into seperate directory path and file name strings. + * + * @param path Full path string. + * @param sep Path seperator character. + * @param list String list to return values in, or null to allocate + * @return java.lang.String[] + */ + public static String[] splitPath(String path, char sep, String[] list) + { + if (path == null) + throw new IllegalArgumentException("Path may not be null"); + + // Create an array of strings to hold the path and file name strings + String[] pathStr = list; + if (pathStr == null) + pathStr = new String[] {"", ""}; + + // Check if the path is valid + if (path.length() > 0) + { + // Check if the path has a trailing seperator, if so then there is no file name. + int pos = path.lastIndexOf(sep); + if (pos == -1 || pos == (path.length() - 1)) + { + // Set the path string in the returned string array + pathStr[0] = path; + } + else + { + // Split the path into directory list and file name strings + pathStr[1] = path.substring(pos + 1); + + if (pos == 0) + pathStr[0] = path.substring(0, pos + 1); + else + pathStr[0] = path.substring(0, pos); + } + } + + // Return the path strings + return pathStr; + } + + /** + * Split the path into all the component directories and filename + * + * @param path String + * @return String[] + */ + public static String[] splitAllPaths(String path) + { + + // Check if the path is valid + + if (path == null || path.length() == 0) + return null; + + // Determine the number of components in the path + + StringTokenizer token = new StringTokenizer(path, DOS_SEPERATOR_STR); + String[] names = new String[token.countTokens()]; + + // Split the path + + int i = 0; + + while (i < names.length && token.hasMoreTokens()) + names[i++] = token.nextToken(); + + // Return the path components + + return names; + } + + /** + * Split a path string into directory path, file name and stream name components + * + * @param path Full path string. + * @return java.lang.String[] + */ + public static String[] splitPathStream(String path) + { + + // Allocate the return list + + String[] pathStr = new String[3]; + + // Split the path into directory path and file/stream name + + FileName.splitPath(path, DOS_SEPERATOR, pathStr); + if (pathStr[1] == null) + return pathStr; + + // Split the file name into file and stream names + + int pos = pathStr[1].indexOf(NTFSStreamSeperator); + + if (pos != -1) + { + + // Split the file/stream name + + pathStr[2] = pathStr[1].substring(pos); + pathStr[1] = pathStr[1].substring(0, pos); + } + + // Return the path components list + + return pathStr; + } + + /** + * Test if a file name contains an NTFS stream name + * + * @param path String + * @return boolean + */ + public static boolean containsStreamName(String path) + { + + // Check if the path contains the stream name seperator character + + if (path.indexOf(NTFSStreamSeperator) != -1) + return true; + return false; + } + + /** + * Normalize the path to uppercase the directory names and keep the case of the file name. + * + * @param path String + * @return String + */ + public final static String normalizePath(String path) + { + + // Split the path into directories and file name, only uppercase the directories to + // normalize + // the path. + + String normPath = path; + + if (path.length() > 3) + { + + // Split the path to seperate the folders/file name + + int pos = path.lastIndexOf(DOS_SEPERATOR); + if (pos != -1) + { + + // Get the path and file name parts, normalize the path + + String pathPart = path.substring(0, pos).toUpperCase(); + String namePart = path.substring(pos); + + // Rebuild the path string + + normPath = pathPart + namePart; + } + } + + // Return the normalized path + + return normPath; + } + + /** + * Make a path relative to the base path for the specified path. + * + * @param basePath String + * @param fullPath String + * @return String + */ + public final static String makeRelativePath(String basePath, String fullPath) + { + + // Check if the base path is the root path + + if (basePath.length() == 0 || basePath.equals(DOS_SEPERATOR_STR)) + { + + // Return the full path, strip any leading seperator + + if (fullPath.length() > 0 && fullPath.charAt(0) == DOS_SEPERATOR) + return fullPath.substring(1); + return fullPath; + } + + // Split the base and full paths into seperate components + + String[] baseNames = splitAllPaths(basePath); + String[] fullNames = splitAllPaths(fullPath); + + // Check that the full path is actually within the base path tree + + if (baseNames != null && baseNames.length > 0 && fullNames != null && fullNames.length > 0 + && baseNames[0].equalsIgnoreCase(fullNames[0]) == false) + return null; + + // Match the path names + + int idx = 0; + + while (idx < baseNames.length && idx < fullNames.length && baseNames[idx].equalsIgnoreCase(fullNames[idx])) + idx++; + + // Build the relative path + + StringBuffer relPath = new StringBuffer(128); + + while (idx < fullNames.length) + { + relPath.append(fullNames[idx++]); + if (idx < fullNames.length) + relPath.append(DOS_SEPERATOR); + } + + // Return the relative path + + return relPath.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/FileOfflineException.java b/source/java/org/alfresco/filesys/server/filesys/FileOfflineException.java new file mode 100644 index 0000000000..b3583623a4 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileOfflineException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.IOException; + +/** + *

    + * This exception may be thrown by a disk interface when the file data is not available due to the + * file being archived or the repository being unavailable. + */ +public class FileOfflineException extends IOException +{ + private static final long serialVersionUID = 3257006574835807795L; + + /** + * Class constructor. + */ + public FileOfflineException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public FileOfflineException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/FileOpenParams.java b/source/java/org/alfresco/filesys/server/filesys/FileOpenParams.java new file mode 100644 index 0000000000..5ac1237492 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileOpenParams.java @@ -0,0 +1,749 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.smb.SharingMode; +import org.alfresco.filesys.smb.WinNT; + +/** + * File Open Parameters Class + *

    + * Contains the details of a file open request. + */ +public class FileOpenParams +{ + // Constants + + public final static String StreamSeparator = ":"; + + // Conversion array for Core/LanMan open actions to NT open action codes + + private static int[] _NTToLMOpenCode = { + FileAction.TruncateExisting + FileAction.CreateNotExist, + FileAction.OpenIfExists, + FileAction.CreateNotExist, + FileAction.OpenIfExists + FileAction.CreateNotExist, + FileAction.TruncateExisting, + FileAction.TruncateExisting + FileAction.CreateNotExist }; + + // File open mode strings + + private static String[] _openMode = { "Supersede", "Open", "Create", "OpenIf", "Overwrite", "OverwriteIf" }; + + // File/directory to be opened + + private String m_path; + + // Stream name + + private String m_stream; + + // File open action + + private int m_openAction; + + // Desired access mode + + private int m_accessMode; + + // File attributes + + private int m_attr; + + // Allocation size + + private long m_allocSize; + + // Shared access flags + + private int m_sharedAccess = SharingMode.READWRITE; + + // Creation date/time + + private long m_createDate; + + // Root directory file id, zero if not specified + + private int m_rootFID; + + // Create options + + private int m_createOptions; + + // Security impersonation level, -1 if not set + + private int m_secLevel; + + // Security flags + + private int m_secFlags; + + // Owner group and user id + + private int m_gid = -1; + private int m_uid = -1; + + // Unix mode + + private int m_mode = -1; + + /** + * Class constructor for Core SMB dialect Open SMB requests + * + * @param path String + * @param openAction int + * @param accessMode int + * @param fileAttr int + */ + public FileOpenParams(String path, int openAction, int accessMode, int fileAttr) + { + + // Parse the file path, split into file name and stream if specified + + parseFileName(path); + + m_openAction = convertToNTOpenAction(openAction); + m_accessMode = convertToNTAccessMode(accessMode); + m_attr = fileAttr; + + // Check if the diectory attribute is set + + if (FileAttribute.isDirectory(m_attr)) + m_createOptions = WinNT.CreateDirectory; + + // No security settings + + m_secLevel = -1; + } + + /** + * Class constructor for Core SMB dialect Open SMB requests + * + * @param path String + * @param openAction int + * @param accessMode int + * @param fileAttr int + * @param gid int + * @param uid int + * @param mode int + */ + public FileOpenParams(String path, int openAction, int accessMode, int fileAttr, int gid, int uid, int mode) + { + + // Parse the file path, split into file name and stream if specified + + parseFileName(path); + + m_openAction = convertToNTOpenAction(openAction); + m_accessMode = convertToNTAccessMode(accessMode); + m_attr = fileAttr; + + // Check if the diectory attribute is set + + if (FileAttribute.isDirectory(m_attr)) + m_createOptions = WinNT.CreateDirectory; + + // No security settings + + m_secLevel = -1; + + m_gid = gid; + m_uid = uid; + m_mode = mode; + } + + /** + * Class constructor for LanMan SMB dialect OpenAndX requests + * + * @param path String + * @param openAction int + * @param accessMode int + * @param searchAttr int + * @param fileAttr int + * @param allocSize int + * @param createDate long + */ + public FileOpenParams(String path, int openAction, int accessMode, int searchAttr, int fileAttr, int allocSize, + long createDate) + { + + // Parse the file path, split into file name and stream if specified + + parseFileName(path); + + m_openAction = convertToNTOpenAction(openAction); + m_accessMode = convertToNTAccessMode(accessMode); + m_attr = fileAttr; + m_sharedAccess = convertToNTSharedMode(accessMode); + m_allocSize = (long) allocSize; + m_createDate = createDate; + + // Check if the diectory attribute is set + + if (FileAttribute.isDirectory(m_attr)) + m_createOptions = WinNT.CreateDirectory; + + // No security settings + + m_secLevel = -1; + } + + /** + * Class constructor for NT SMB dialect NTCreateAndX requests + * + * @param path String + * @param openAction int + * @param accessMode int + * @param attr int + * @param sharedAccess int + * @param allocSize long + * @param createOption int + * @param rootFID int + * @param secLevel int + * @param secFlags int + */ + public FileOpenParams(String path, int openAction, int accessMode, int attr, int sharedAccess, long allocSize, + int createOption, int rootFID, int secLevel, int secFlags) + { + + // Parse the file path, split into file name and stream if specified + + parseFileName(path); + + m_openAction = openAction; + m_accessMode = accessMode; + m_attr = attr; + m_sharedAccess = sharedAccess; + m_allocSize = allocSize; + m_createOptions = createOption; + m_rootFID = rootFID; + m_secLevel = secLevel; + m_secFlags = secFlags; + + // Make sure the directory attribute is set if the create directory option is set + + if ((createOption & WinNT.CreateDirectory) != 0 && (m_attr & FileAttribute.Directory) == 0) + m_attr += FileAttribute.Directory; + } + + /** + * Return the path to be opened/created + * + * @return String + */ + public final String getPath() + { + return m_path; + } + + /** + * Return the full path to be opened/created, including the stream + * + * @return String + */ + public final String getFullPath() + { + if (isStream()) + return m_path + m_stream; + else + return m_path; + } + + /** + * Return the file attributes + * + * @return int + */ + public final int getAttributes() + { + return m_attr; + } + + /** + * Return the allocation size, or zero if not specified + * + * @return long + */ + public final long getAllocationSize() + { + return m_allocSize; + } + + /** + * Determine if a creation date/time has been specified + * + * @return boolean + */ + public final boolean hasCreationDateTime() + { + return m_createDate != 0L ? true : false; + } + + /** + * Return the file creation date/time + * + * @return long + */ + public final long getCreationDateTime() + { + return m_createDate; + } + + /** + * Return the open/create file/directory action All actions are mapped to the FileAction.NTxxx + * action codes. + * + * @return int + */ + public final int getOpenAction() + { + return m_openAction; + } + + /** + * Return the root directory file id, or zero if not specified + * + * @return int + */ + public final int getRootDirectoryFID() + { + return m_rootFID; + } + + /** + * Return the stream name + * + * @return String + */ + public final String getStreamName() + { + return m_stream; + } + + /** + * Check if the specified create option is enabled, specified in the WinNT class. + * + * @param flag int + * @return boolean + */ + public final boolean hasCreateOption(int flag) + { + return (m_createOptions & flag) != 0 ? true : false; + } + + /** + * Check if a file stream has been specified in the path to be created/opened + * + * @return boolean + */ + public final boolean isStream() + { + return m_stream != null ? true : false; + } + + /** + * Determine if the file is to be opened read-only + * + * @return boolean + */ + public final boolean isReadOnlyAccess() + { + // Check if read-only or execute access has been requested + + if (( m_accessMode & AccessMode.NTReadWrite) == AccessMode.NTRead || + (m_accessMode & AccessMode.NTExecute) != 0) + return true; + return false; + } + + /** + * Determine if the file is to be opened write-only + * + * @return boolean + */ + public final boolean isWriteOnlyAccess() + { + return (m_accessMode & AccessMode.NTReadWrite) == AccessMode.NTWrite ? true : false; + } + + /** + * Determine if the file is to be opened read/write + * + * @return boolean + */ + public final boolean isReadWriteAccess() + { + return (m_accessMode & AccessMode.NTReadWrite) == AccessMode.NTReadWrite ? true : false; + } + + /** + * Check for a particular access mode + * + * @param mode int + * @return boolean + */ + public final boolean hasAccessMode(int mode) + { + return (m_accessMode & mode) == mode ? true : false; + } + + /** + * Determine if the target of the create/open is a directory + * + * @return boolean + */ + public final boolean isDirectory() + { + return hasCreateOption(WinNT.CreateDirectory); + } + + /** + * Determine if the file will be accessed sequentially only + * + * @return boolean + */ + public final boolean isSequentialAccessOnly() + { + return hasCreateOption(WinNT.CreateSequential); + } + + /** + * Determine if the file should be deleted when closed + * + * @return boolean + */ + public final boolean isDeleteOnClose() + { + return hasCreateOption(WinNT.CreateDeleteOnClose); + } + + /** + * Determine if write-through mode is enabled (buffering is not allowed if enabled) + * + * @return boolean + */ + public final boolean isWriteThrough() + { + return hasCreateOption(WinNT.CreateWriteThrough); + } + + /** + * Determine if the open mode should overwrite/truncate an existing file + * + * @return boolean + */ + public final boolean isOverwrite() + { + if (getOpenAction() == FileAction.NTSupersede || getOpenAction() == FileAction.NTOverwrite + || getOpenAction() == FileAction.NTOverwriteIf) + return true; + return false; + } + + /** + * Return the shared access mode, zero equals allow any shared access + * + * @return int + */ + public final int getSharedAccess() + { + return m_sharedAccess; + } + + /** + * Determine if security impersonation is enabled + * + * @return boolean + */ + public final boolean hasSecurityLevel() + { + return m_secLevel != -1 ? true : false; + } + + /** + * Return the security impersonation level. Levels are defined in the WinNT class. + * + * @return int + */ + public final int getSecurityLevel() + { + return m_secLevel; + } + + /** + * Determine if the security context tracking flag is enabled + * + * @return boolean + */ + public final boolean hasSecurityContextTracking() + { + return (m_secFlags & WinNT.SecurityContextTracking) != 0 ? true : false; + } + + /** + * Determine if the security effective only flag is enabled + * + * @return boolean + */ + public final boolean hasSecurityEffectiveOnly() + { + return (m_secFlags & WinNT.SecurityEffectiveOnly) != 0 ? true : false; + } + + /** + * Determine if the group id has been set + * + * @return boolean + */ + public final boolean hasGid() + { + return m_gid != -1 ? true : false; + } + + /** + * Return the owner group id + * + * @return int + */ + public final int getGid() + { + return m_gid; + } + + /** + * Determine if the user id has been set + * + * @return boolean + */ + public final boolean hasUid() + { + return m_uid != -1 ? true : false; + } + + /** + * Return the owner user id + * + * @return int + */ + public final int getUid() + { + return m_uid; + } + + /** + * Determine if the mode has been set + * + * @return boolean + */ + public final boolean hasMode() + { + return m_mode != -1 ? true : false; + } + + /** + * Return the Unix mode + * + * @return int + */ + public final int getMode() + { + return m_mode; + } + + /** + * Set the Unix mode + * + * @param mode int + */ + public final void setMode(int mode) + { + m_mode = mode; + } + + /** + * Set a create option flag + * + * @param flag int + */ + public final void setCreateOption(int flag) + { + m_createOptions = m_createOptions | flag; + } + + /** + * Convert a Core/LanMan access mode to an NT access mode + * + * @param accessMode int + * @return int + */ + private final int convertToNTAccessMode(int accessMode) + { + + // Convert the Core/LanMan SMB dialect format access mode value to an NT access mode + + int mode = 0; + + switch (AccessMode.getAccessMode(accessMode)) + { + case AccessMode.ReadOnly: + mode = AccessMode.NTRead; + break; + case AccessMode.WriteOnly: + mode = AccessMode.NTWrite; + break; + case AccessMode.ReadWrite: + mode = AccessMode.NTReadWrite; + break; + } + return mode; + } + + /** + * Convert a Core/LanMan open action to an NT open action + * + * @param openAction int + * @return int + */ + private final int convertToNTOpenAction(int openAction) + { + + // Convert the Core/LanMan SMB dialect open action to an NT open action + + int action = FileAction.NTOpen; + + for (int i = 0; i < _NTToLMOpenCode.length; i++) + { + if (_NTToLMOpenCode[i] == openAction) + action = i; + } + return action; + } + + /** + * Convert a Core/LanMan shared access to NT sharing flags + * + * @param sharedAccess int + * @return int + */ + private final int convertToNTSharedMode(int sharedAccess) + { + + // Get the shared access value from the access mask + + int shr = AccessMode.getSharingMode(sharedAccess); + int ret = SharingMode.READWRITE; + + switch (shr) + { + case AccessMode.Exclusive: + ret = SharingMode.NOSHARING; + break; + case AccessMode.DenyRead: + ret = SharingMode.WRITE; + break; + case AccessMode.DenyWrite: + ret = SharingMode.READ; + break; + } + return ret; + } + + /** + * Parse a file name to split the main file name/path and stream name + * + * @param fileName String + */ + private final void parseFileName(String fileName) + { + + // Check if the file name contains a stream name + + int pos = fileName.indexOf(StreamSeparator); + if (pos == -1) + { + m_path = fileName; + return; + } + + // Split the main file name and stream name + + m_path = fileName.substring(0, pos); + m_stream = fileName.substring(pos); + } + + /** + * Return the file open parameters as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + + str.append(getPath()); + + str.append(","); + str.append(_openMode[getOpenAction()]); + str.append(",acc=0x"); + str.append(Integer.toHexString(m_accessMode)); + str.append(",attr=0x"); + str.append(Integer.toHexString(getAttributes())); + str.append(",alloc="); + str.append(getAllocationSize()); + str.append(",share=0x"); + str.append(Integer.toHexString(getSharedAccess())); + + if (getRootDirectoryFID() != 0) + { + str.append(",fid="); + str.append(getRootDirectoryFID()); + } + + if (hasCreationDateTime()) + { + str.append(",cdate="); + str.append(getCreationDateTime()); + } + + if (m_createOptions != 0) + { + str.append(",copt=0x"); + str.append(Integer.toHexString(m_createOptions)); + } + + if (hasSecurityLevel()) + { + str.append(",seclev="); + str.append(getSecurityLevel()); + str.append(",secflg=0x"); + str.append(Integer.toHexString(m_secFlags)); + } + str.append("]"); + + if (hasGid() || hasUid()) + { + str.append(",gid="); + str.append(getGid()); + str.append(",uid="); + str.append(getUid()); + str.append(",mode="); + str.append(getMode()); + } + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/FileSharingException.java b/source/java/org/alfresco/filesys/server/filesys/FileSharingException.java new file mode 100644 index 0000000000..8ade565aac --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileSharingException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * File sharing exception class. + */ +public class FileSharingException extends java.io.IOException +{ + private static final long serialVersionUID = 3258130241309260085L; + + /** + * Class constructor + */ + public FileSharingException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public FileSharingException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/FileStatus.java b/source/java/org/alfresco/filesys/server/filesys/FileStatus.java new file mode 100644 index 0000000000..7f47de5eb8 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileStatus.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * File Status Class + */ +public class FileStatus +{ + + // File status constants + + public final static int Unknown = -1; + public final static int NotExist = 0; + public final static int FileExists = 1; + public final static int DirectoryExists = 2; + + /** + * Return the file status as a string + * + * @param sts int + * @return String + */ + public final static String asString(int sts) + { + + // Convert the status to a string + + String ret = ""; + + switch (sts) + { + case Unknown: + ret = "Unknown"; + break; + case NotExist: + ret = "NotExist"; + break; + case FileExists: + ret = "FileExists"; + break; + case DirectoryExists: + ret = "DirExists"; + break; + } + + return ret; + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/FileSystem.java b/source/java/org/alfresco/filesys/server/filesys/FileSystem.java new file mode 100644 index 0000000000..fb524ca33f --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/FileSystem.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * Filesystem Attributes Class + *

    + * Contains constant attributes used to define filesystem features available. The values are taken + * from the SMB/CIFS protocol query filesystem call. + */ +public final class FileSystem +{ + + // Filesystem attributes + + public static final int CaseSensitiveSearch = 0x00000001; + public static final int CasePreservedNames = 0x00000002; + public static final int UnicodeOnDisk = 0x00000004; + public static final int PersistentACLs = 0x00000008; + public static final int FileCompression = 0x00000010; + public static final int VolumeQuotas = 0x00000020; + public static final int SparseFiles = 0x00000040; + public static final int ReparsePoints = 0x00000080; + public static final int RemoteStorage = 0x00000100; + public static final int VolumeIsCompressed = 0x00008000; + public static final int ObjectIds = 0x00010000; + public static final int Encryption = 0x00020000; + + // Filesystem type strings + + public static final String TypeFAT = "FAT"; + public static final String TypeNTFS = "NTFS"; +} diff --git a/source/java/org/alfresco/filesys/server/filesys/HomeShareMapper.java b/source/java/org/alfresco/filesys/server/filesys/HomeShareMapper.java new file mode 100644 index 0000000000..75c8248eb8 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/HomeShareMapper.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +package org.alfresco.filesys.server.filesys; + +import java.util.Enumeration; + +import org.alfresco.config.ConfigElement; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.auth.InvalidUserException; +import org.alfresco.filesys.server.config.InvalidConfigurationException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.ShareMapper; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Home Share Mapper Class + * + *

    Maps disk share lookup requests to the list of shares defined in the server + * configuration and provides a dynamic home share mapped to the users home node. + * + * @author GKSpencer + */ +public class HomeShareMapper implements ShareMapper +{ + // Logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Home folder share name + + public static final String HOME_FOLDER_SHARE = "HOME"; + + // Server configuration + + private ServerConfiguration m_config; + + // Home folder share name + + private String m_homeShareName = HOME_FOLDER_SHARE; + + // Debug enable flag + + private boolean m_debug; + + /** + * Default constructor + */ + public HomeShareMapper() + { + } + + /** + * Initialize the share mapper + * + * @param config ServerConfiguration + * @param params ConfigElement + * @exception InvalidConfigurationException + */ + public void initializeMapper(ServerConfiguration config, ConfigElement params) throws InvalidConfigurationException + { + // Save the server configuration + + m_config = config; + + // Check if the home share name has been specified + + String homeName = params.getAttribute("name"); + if ( homeName != null && homeName.length() > 0) + m_homeShareName = homeName; + + // Check if debug is enabled + + if (params != null && params.getChild("debug") != null) + m_debug = true; + } + + /** + * Check if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Return the home folder share name + * + * @return String + */ + public final String getHomeFolderName() + { + return m_homeShareName; + } + + /** + * Return the list of available shares. + * + * @param host String + * @param sess SrvSession + * @param allShares boolean + * @return SharedDeviceList + */ + public SharedDeviceList getShareList(String host, SrvSession sess, boolean allShares) + { + // Check if the user has a home folder, and the session does not currently have any + // dynamic shares defined + + if ( sess != null && sess.hasClientInformation() && sess.hasDynamicShares() == false) + { + ClientInfo client = sess.getClientInformation(); + if ( client.hasHomeFolder()) + { + // Create the home folder share + + DiskSharedDevice homeShare = createHomeDiskShare(client); + sess.addDynamicShare(homeShare); + + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Added " + getHomeFolderName() + " share to list of shares for " + client.getUserName()); + } + } + + // Make a copy of the global share list and add the per session dynamic shares + + SharedDeviceList shrList = new SharedDeviceList(m_config.getShares()); + + if ( sess != null && sess.hasDynamicShares()) { + + // Add the per session dynamic shares + + shrList.addShares(sess.getDynamicShareList()); + } + + // Remove unavailable shares from the list and return the list + + if ( allShares == false) + shrList.removeUnavailableShares(); + return shrList; + } + + /** + * Find a share using the name and type for the specified client. + * + * @param host String + * @param name String + * @param typ int + * @param sess SrvSession + * @param create boolean + * @return SharedDevice + * @exception InvalidUserException + */ + public SharedDevice findShare(String tohost, String name, int typ, SrvSession sess, boolean create) + throws Exception + { + + // Check for the special HOME disk share + + SharedDevice share = null; + + if (( typ == ShareType.DISK || typ == ShareType.UNKNOWN) && name.equalsIgnoreCase(getHomeFolderName()) && + sess.getClientInformation() != null) { + + // Get the client details + + ClientInfo client = sess.getClientInformation(); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Map share " + name + ", type=" + ShareType.TypeAsString(typ) + ", client=" + client); + + // Check if the user has a home folder node + + if ( client != null && client.hasHomeFolder()) { + + // Check if the share has already been created for the session + + if ( sess.hasDynamicShares()) { + + // Check if the required share exists in the sessions dynamic share list + + share = sess.getDynamicShareList().findShare(name, typ, false); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug(" Reusing existing dynamic share for " + name); + } + + // Check if we found a share, if not then create a new dynamic share for the home directory + + if ( share == null && create == true) { + + // Create the home share mapped to the users home folder + + DiskSharedDevice diskShare = createHomeDiskShare(client); + + // Add the new share to the sessions dynamic share list + + sess.addDynamicShare(diskShare); + share = diskShare; + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug(" Mapped share " + name + " to " + client.getHomeFolder()); + } + } + else + throw new InvalidUserException("No home directory"); + } + else { + + // Find the required share by name/type. Use a case sensitive search first, if that fails use a case + // insensitive search. + + share = m_config.getShares().findShare(name, typ, false); + + if ( share == null) { + + // Try a case insensitive search for the required share + + share = m_config.getShares().findShare(name, typ, true); + } + } + + // Check if the share is available + + if ( share != null && share.getContext() != null && share.getContext().isAvailable() == false) + share = null; + + // Return the shared device, or null if no matching device was found + + return share; + } + + /** + * Delete temporary shares for the specified session + * + * @param sess SrvSession + */ + public void deleteShares(SrvSession sess) + { + + // Check if the session has any dynamic shares + + if ( sess.hasDynamicShares() == false) + return; + + // Delete the dynamic shares + + SharedDeviceList shares = sess.getDynamicShareList(); + Enumeration enm = shares.enumerateShares(); + + while ( enm.hasMoreElements()) { + + // Get the current share from the list + + SharedDevice shr = (SharedDevice) enm.nextElement(); + + // Close the shared device + + shr.getContext().CloseContext(); + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug("Deleted dynamic share " + shr); + } + + // Clear the dynamic share list + + shares.removeAllShares(); + } + + /** + * Close the share mapper, release any resources. + */ + public void closeMapper() + { + // TODO Auto-generated method stub + + } + + /** + * Create a disk share for the home folder + * + * @param client ClientInfo + * @return DiskSharedDevice + */ + private final DiskSharedDevice createHomeDiskShare(ClientInfo client) + { + // Create the disk driver and context + + DiskInterface diskDrv = m_config.getDiskInterface(); + DiskDeviceContext diskCtx = new DiskDeviceContext(client.getHomeFolder().toString()); + + // Default the filesystem to look like an 80Gb sized disk with 90% free space + + diskCtx.setDiskInformation(new SrvDiskInfo(2560, 64, 512, 2304)); + + // Create a temporary shared device for the users home directory + + return new DiskSharedDevice(getHomeFolderName(), diskDrv, diskCtx, SharedDevice.Temporary); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/MediaOfflineException.java b/source/java/org/alfresco/filesys/server/filesys/MediaOfflineException.java new file mode 100644 index 0000000000..41566de832 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/MediaOfflineException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.IOException; + +/** + * Media Offline Exception Class + *

    + * This exception may be thrown by a disk interface when a file/folder is not available due to the + * storage media being offline, repository being unavailable, database unavailable or inaccessible + * or similar condition. + */ +public class MediaOfflineException extends IOException +{ + private static final long serialVersionUID = 3544956554064704306L; + + /** + * Class constructor. + */ + public MediaOfflineException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public MediaOfflineException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/NetworkFile.java b/source/java/org/alfresco/filesys/server/filesys/NetworkFile.java new file mode 100644 index 0000000000..e4a6160f01 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/NetworkFile.java @@ -0,0 +1,836 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.IOException; + +import org.alfresco.filesys.locking.FileLock; +import org.alfresco.filesys.locking.FileLockList; + +/** + *

    + * The network file represents a file or directory on a filesystem. The server keeps track of the + * open files on a per session basis. + *

    + * This class may be extended as required by your own disk driver class. + */ +public abstract class NetworkFile +{ + + // Granted file access types + + public static final int READONLY = 0; + public static final int WRITEONLY = 1; + public static final int READWRITE = 2; + + // File status flags + + public static final int IOPending = 0x0001; + public static final int DeleteOnClose = 0x0002; + + // File identifier and parent directory identifier + + protected int m_fid; + protected int m_dirId; + + // Unique file identifier + + protected long m_uniqueId; + + // File/directory name + + protected String m_name; + + // Stream name and id + + protected String m_streamName; + protected int m_streamId; + + // Full name, relative to the share + + protected String m_fullName; + + // File attributes + + protected int m_attrib; + + // File size + + protected long m_fileSize; + + // File creation/modify/last access date/time + + protected long m_createDate; + protected long m_modifyDate; + protected long m_accessDate; + + // Granted file access type + + protected int m_grantedAccess; + + // Flag to indicate that the file has been closed + + protected boolean m_closed = true; + + // Count of write requests to the file, used to determine if the file size may have changed + + protected int m_writeCount; + + // List of locks on this file by this session. The lock object will almost certainly be + // referenced elsewhere depending upon the LockManager implementation used. If locking support is not + // enabled for the DiskInterface implementation the lock list will not be allocated. + // + // This lock list is used to release locks on the file if the session abnormally terminates or + // closes the file without releasing all locks. + + private FileLockList m_lockList; + + // File status flags + + private int m_flags; + + /** + * Create a network file object with the specified file identifier. + * + * @param fid int + */ + public NetworkFile(int fid) + { + m_fid = fid; + } + + /** + * Create a network file with the specified file and parent directory ids + * + * @param fid int + * @param did int + */ + public NetworkFile(int fid, int did) + { + m_fid = fid; + m_dirId = did; + } + + /** + * Create a network file with the specified file id, stream id and parent directory id + * + * @param fid int + * @param stid int + * @param did int + */ + public NetworkFile(int fid, int stid, int did) + { + m_fid = fid; + m_streamId = stid; + m_dirId = did; + } + + /** + * Create a network file object with the specified file/directory name. + * + * @param name File name string. + */ + public NetworkFile(String name) + { + m_name = name; + } + + /** + * Return the parent directory identifier + * + * @return int + */ + public final int getDirectoryId() + { + return m_dirId; + } + + /** + * Return the file attributes. + * + * @return int + */ + public final int getFileAttributes() + { + return m_attrib; + } + + /** + * Return the file identifier. + * + * @return int + */ + public final int getFileId() + { + return m_fid; + } + + /** + * Get the file size, in bytes. + * + * @return long + */ + public final long getFileSize() + { + return m_fileSize; + } + + /** + * Get the file size, in bytes. + * + * @return int + */ + public final int getFileSizeInt() + { + return (int) (m_fileSize & 0x0FFFFFFFFL); + } + + /** + * Return the full name, relative to the share. + * + * @return java.lang.String + */ + public final String getFullName() + { + return m_fullName; + } + + /** + * Return the full name including the stream name, relative to the share. + * + * @return java.lang.String + */ + public final String getFullNameStream() + { + if (isStream()) + return m_fullName + m_streamName; + else + return m_fullName; + } + + /** + * Return the granted file access mode. + */ + public final int getGrantedAccess() + { + return m_grantedAccess; + } + + /** + * Return the file/directory name. + * + * @return java.lang.String + */ + public String getName() + { + return m_name; + } + + /** + * Return the stream id, zero indicates the main file stream + * + * @return int + */ + public final int getStreamId() + { + return m_streamId; + } + + /** + * Return the stream name, if this is a stream + * + * @return String + */ + public final String getStreamName() + { + return m_streamName; + } + + /** + * Return the unique file identifier + * + * @return long + */ + public final long getUniqueId() + { + return m_uniqueId; + } + + /** + * Determine if the file has been closed. + * + * @return boolean + */ + public final boolean isClosed() + { + return m_closed; + } + + /** + * Return the directory file attribute status. + * + * @return true if the file is a directory, else false. + */ + + public final boolean isDirectory() + { + return (m_attrib & FileAttribute.Directory) != 0 ? true : false; + } + + /** + * Return the hidden file attribute status. + * + * @return true if the file is hidden, else false. + */ + + public final boolean isHidden() + { + return (m_attrib & FileAttribute.Hidden) != 0 ? true : false; + } + + /** + * Return the read-only file attribute status. + * + * @return true if the file is read-only, else false. + */ + + public final boolean isReadOnly() + { + return (m_attrib & FileAttribute.ReadOnly) != 0 ? true : false; + } + + /** + * Return the system file attribute status. + * + * @return true if the file is a system file, else false. + */ + + public final boolean isSystem() + { + return (m_attrib & FileAttribute.System) != 0 ? true : false; + } + + /** + * Return the archived attribute status + * + * @return boolean + */ + public final boolean isArchived() + { + return (m_attrib & FileAttribute.Archive) != 0 ? true : false; + } + + /** + * Check if this is a stream file + * + * @return boolean + */ + public final boolean isStream() + { + return m_streamName != null ? true : false; + } + + /** + * Check if there are active locks on this file by this session + * + * @return boolean + */ + public final boolean hasLocks() + { + if (m_lockList != null && m_lockList.numberOfLocks() > 0) + return true; + return false; + } + + /** + * Check for NT attributes + * + * @param attr int + * @return boolean + */ + public final boolean hasNTAttribute(int attr) + { + return (m_attrib & attr) == attr ? true : false; + } + + /** + * Determine if the file access date/time is valid + * + * @return boolean + */ + public final boolean hasAccessDate() + { + return m_accessDate != 0L ? true : false; + } + + /** + * Return the file access date/time + * + * @return long + */ + public final long getAccessDate() + { + return m_accessDate; + } + + /** + * Determine if the file creation date/time is valid + * + * @return boolean + */ + public final boolean hasCreationDate() + { + return m_createDate != 0L ? true : false; + } + + /** + * Return the file creation date/time + * + * @return long + */ + public final long getCreationDate() + { + return m_createDate; + } + + /** + * Check if the delete on close flag has been set for this file + * + * @return boolean + */ + public final boolean hasDeleteOnClose() + { + return (m_flags & DeleteOnClose) != 0 ? true : false; + } + + /** + * Check if the file has an I/O request pending + * + * @return boolean + */ + public final boolean hasIOPending() + { + return (m_flags & IOPending) != 0 ? true : false; + } + + /** + * Determine if the file modification date/time is valid + * + * @return boolean + */ + public boolean hasModifyDate() + { + return m_modifyDate != 0L ? true : false; + } + + /** + * Return the file modify date/time + * + * @return long + */ + public final long getModifyDate() + { + return m_modifyDate; + } + + /** + * Get the write count for the file + * + * @return int + */ + public final int getWriteCount() + { + return m_writeCount; + } + + /** + * Increment the write count + */ + public final void incrementWriteCount() + { + m_writeCount++; + } + + /** + * Set the file attributes, as specified by the SMBFileAttribute class. + * + * @param attrib int + */ + public final void setAttributes(int attrib) + { + m_attrib = attrib; + } + + /** + * Set, or clear, the delete on close flag + * + * @param del boolean + */ + public final void setDeleteOnClose(boolean del) + { + setStatusFlag(DeleteOnClose, del); + } + + /** + * Set the parent directory identifier + * + * @param dirId int + */ + public final void setDirectoryId(int dirId) + { + m_dirId = dirId; + } + + /** + * Set the file identifier. + * + * @param fid int + */ + public final void setFileId(int fid) + { + m_fid = fid; + } + + /** + * Set the file size. + * + * @param siz long + */ + public final void setFileSize(long siz) + { + m_fileSize = siz; + } + + /** + * Set the file size. + * + * @param siz int + */ + public final void setFileSize(int siz) + { + m_fileSize = (long) siz; + } + + /** + * Set the full file name, relative to the share. + * + * @param name java.lang.String + */ + public final void setFullName(String name) + { + m_fullName = name; + } + + /** + * Set the granted file access mode. + * + * @param mode int + */ + public final void setGrantedAccess(int mode) + { + m_grantedAccess = mode; + } + + /** + * Set the file name. + * + * @param name String + */ + public final void setName(String name) + { + m_name = name; + } + + /** + * set/clear the I/O pending flag + * + * @param pending boolean + */ + public final void setIOPending(boolean pending) + { + setStatusFlag(IOPending, pending); + } + + /** + * Set the stream id + * + * @param id int + */ + public final void setStreamId(int id) + { + m_streamId = id; + } + + /** + * Set the stream name + * + * @param name String + */ + public final void setStreamName(String name) + { + m_streamName = name; + } + + /** + * Set the file closed state. + * + * @param b boolean + */ + public final synchronized void setClosed(boolean b) + { + m_closed = b; + } + + /** + * Set the file access date/time + * + * @param dattim long + */ + public final void setAccessDate(long dattim) + { + m_accessDate = dattim; + } + + /** + * Set the file creation date/time + * + * @param dattim long + */ + public final void setCreationDate(long dattim) + { + m_createDate = dattim; + } + + /** + * Set the file modification date/time + * + * @param dattim long + */ + public final void setModifyDate(long dattim) + { + m_modifyDate = dattim; + } + + /** + * Set/clear a file status flag + * + * @param flag int + * @param sts boolean + */ + protected final synchronized void setStatusFlag(int flag, boolean sts) + { + boolean state = (m_flags & flag) != 0; + if (sts == true && state == false) + m_flags += flag; + else if (sts == false && state == true) + m_flags -= flag; + } + + /** + * Add a lock to the active lock list + * + * @param lock FileLock + */ + public final synchronized void addLock(FileLock lock) + { + + // Check if the lock list has been allocated + + if (m_lockList == null) + m_lockList = new FileLockList(); + + // Add the lock + + m_lockList.addLock(lock); + } + + /** + * Remove a lock from the active lock list + * + * @param lock FileLock + */ + public final synchronized void removeLock(FileLock lock) + { + + // Check if the lock list is allocated + + if (m_lockList == null) + return; + + // Remove the lock + + m_lockList.removeLock(lock); + } + + /** + * Remove all locks from the lock list + */ + public final synchronized void removeAllLocks() + { + + // Check if the lock list is valid + + if (m_lockList != null) + m_lockList.removeAllLocks(); + } + + /** + * Return the count of active locks + * + * @return int + */ + public final int numberOfLocks() + { + + // Check if the lock list is allocated + + if (m_lockList == null) + return 0; + return m_lockList.numberOfLocks(); + } + + /** + * Get the details of an active lock from the list + * + * @param idx int + * @return FileLock + */ + public final FileLock getLockAt(int idx) + { + + // Check if the lock list is allocated and the index is valid + + if (m_lockList != null) + return m_lockList.getLockAt(idx); + + // Invalid index or lock list not valid + + return null; + } + + /** + * Return the lock list + * + * @return FileLockList + */ + public final FileLockList getLockList() + { + return m_lockList; + } + + /** + * Set the unique file identifier + * + * @param id long + */ + protected final void setUniqueId(long id) + { + m_uniqueId = id; + } + + /** + * Set the unique id using the file and directory id + * + * @param fid int + * @param did int + */ + protected final void setUniqueId(int fid, int did) + { + long ldid = (long) did; + long lfid = (long) fid; + m_uniqueId = (ldid << 32) + lfid; + } + + /** + * Set the unique id using the full path string + * + * @param path String + */ + protected final void setUniqueId(String path) + { + m_uniqueId = (long) path.toUpperCase().hashCode(); + } + + /** + * Open the file + * + * @param createFlag boolean + * @exception IOException + */ + public abstract void openFile(boolean createFlag) throws IOException; + + /** + * Read from the file. + * + * @param buf byte[] + * @param len int + * @param pos int + * @param fileOff long + * @return Length of data read. + * @exception IOException + */ + public abstract int readFile(byte[] buf, int len, int pos, long fileOff) throws java.io.IOException; + + /** + * Write a block of data to the file. + * + * @param buf byte[] + * @param len int + * @param pos int + * @param fileOff long + * @exception IOException + */ + public abstract void writeFile(byte[] buf, int len, int pos, long fileOff) throws java.io.IOException; + + /** + * Seek to the specified file position. + * + * @param pos long + * @param typ int + * @return int + * @exception IOException + */ + public abstract long seekFile(long pos, int typ) throws IOException; + + /** + * Flush any buffered output to the file + * + * @throws IOException + */ + public abstract void flushFile() throws IOException; + + /** + * Truncate the file to the specified file size + * + * @param siz long + * @exception IOException + */ + public abstract void truncateFile(long siz) throws IOException; + + /** + * Close the database file + */ + public abstract void closeFile() throws IOException; + + /** + * Temporary method + */ + public void close() throws IOException + { + closeFile(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/NetworkFileServer.java b/source/java/org/alfresco/filesys/server/filesys/NetworkFileServer.java new file mode 100644 index 0000000000..760aebaeb9 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/NetworkFileServer.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.server.NetworkServer; +import org.alfresco.filesys.server.config.ServerConfiguration; + +/** + * Network File Server Class + *

    + * Base class for all network file servers. + */ +public abstract class NetworkFileServer extends NetworkServer +{ + + /** + * Class constructor + * + * @param proto String + * @param serviceRegistry repository connection + * @param config ServerConfiguration + */ + public NetworkFileServer(String proto, ServerConfiguration config) + { + super(proto, config); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/NotifyChange.java b/source/java/org/alfresco/filesys/server/filesys/NotifyChange.java new file mode 100644 index 0000000000..e313790918 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/NotifyChange.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + * Notify Change Transaction Class + */ +public class NotifyChange +{ + + // Change notification filter flags + + public final static int FileName = 0x0001; + public final static int DirectoryName = 0x0002; + public final static int Attributes = 0x0004; + public final static int Size = 0x0008; + public final static int LastWrite = 0x0010; + public final static int LastAccess = 0x0020; + public final static int Creation = 0x0040; + public final static int Security = 0x0100; + + // Change notification actions + + public final static int ActionAdded = 1; + public final static int ActionRemoved = 2; + public final static int ActionModified = 3; + public final static int ActionRenamedOldName = 4; + public final static int ActionRenamedNewName = 5; + public final static int ActionAddedStream = 6; + public final static int ActionRemovedStream = 7; + public final static int ActionModifiedStream = 8; + + // Change notification action names + + private final static String[] _actnNames = { "Added", "Removed", "Modified", "RenamedOldName", "RenamedNewName", + "AddedStream", "RemovedStream", "ModifiedStream" }; + + /** + * Return the change notification action as a string + * + * @param action int + * @return String + */ + public static final String getActionAsString(int action) + { + + // Range check the action + + if (action <= 0 || action > _actnNames.length) + return "Unknown"; + + // Return the action as a string + + return _actnNames[action - 1]; + } + + /** + * Return the change notification filter flag as a string. This method assumes a single flag is + * set. + * + * @param filter int + * @return String + */ + public static final String getFilterAsString(int filter) + { + + // Check if there are any flags set + + if (filter == 0) + return ""; + + // Determine the filter type + + String filtStr = null; + + switch (filter) + { + case FileName: + filtStr = "FileName"; + break; + case DirectoryName: + filtStr = "DirectoryName"; + break; + case Attributes: + filtStr = "Attributes"; + break; + case Size: + filtStr = "Size"; + break; + case LastWrite: + filtStr = "LastWrite"; + break; + case LastAccess: + filtStr = "LastAccess"; + break; + case Creation: + filtStr = "Creation"; + break; + case Security: + filtStr = "Security"; + break; + } + + // Return the filter type string + + return filtStr; + } + + /** + * Return the change notification filter flags as a string. + * + * @param filter int + * @return String + */ + public static final String getFilterFlagsAsString(int filter) + { + + // Check if there are any flags set + + if (filter == 0) + return ""; + + // Build the filter flags string + + StringBuffer filtStr = new StringBuffer(); + int i = 0x0001; + + while (i < Security) + { + + // Check if the current filter flag is set + + if ((filter & i) != 0) + { + + // Get the filter flag name + + String name = getFilterAsString(i); + if (name != null) + { + if (filtStr.length() > 0) + filtStr.append(","); + filtStr.append(name); + } + } + + // Update the filter flag mask + + i = i << 1; + } + + // Return the filter flags string + + return filtStr.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/PathNotFoundException.java b/source/java/org/alfresco/filesys/server/filesys/PathNotFoundException.java new file mode 100644 index 0000000000..0c5ff22419 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/PathNotFoundException.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.io.IOException; + +/** + * Path Not Found Exception Class + *

    + * Indicates that the upper part of a path does not exist, as opposed to the file/folder at the end + * of the path. + */ +public class PathNotFoundException extends IOException +{ + private static final long serialVersionUID = 4050768191053378616L; + + /** + * Class constructor. + */ + public PathNotFoundException() + { + super(); + } + + /** + * Class constructor. + * + * @param s java.lang.String + */ + public PathNotFoundException(String s) + { + super(s); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/SearchContext.java b/source/java/org/alfresco/filesys/server/filesys/SearchContext.java new file mode 100644 index 0000000000..98c1d4908c --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/SearchContext.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * The search context represents the state of an active search by a disk interface based class. The + * context is used to continue a search across multiple requests. + */ +public abstract class SearchContext +{ + + // Maximum number of files to return per search request. + + private int m_maxFiles; + + // Tree identifier that this search is associated with + + private int m_treeId; + + // Search string + + private String m_searchStr; + + // Flags + + private int m_flags; + + /** + * Default constructor. + */ + public SearchContext() + { + } + + /** + * Construct a new search context. + * + * @param maxFiles int + * @param treeId int + */ + protected SearchContext(int maxFiles, int treeId) + { + m_maxFiles = maxFiles; + m_treeId = treeId; + } + + /** + * Close the search. + */ + public void closeSearch() + { + } + + /** + * Return the search context flags. + * + * @return int + */ + public final int getFlags() + { + return m_flags; + } + + /** + * Return the maximum number of files that should be returned per search request. + * + * @return int + */ + public final int getMaximumFiles() + { + return m_maxFiles; + } + + /** + * Return the resume id for the current file/directory in the search. + * + * @return int + */ + public abstract int getResumeId(); + + /** + * Return the search string, used for resume keys in some SMB dialects. + * + * @return java.lang.String + */ + public final String getSearchString() + { + return m_searchStr != null ? m_searchStr : ""; + } + + /** + * Return the tree identifier of the tree connection that this search is associated with. + * + * @return int + */ + public final int getTreeId() + { + return m_treeId; + } + + /** + * Determine if there are more files for the active search. + * + * @return boolean + */ + public abstract boolean hasMoreFiles(); + + /** + * Return file information for the next file in the active search. Returns false if the search + * is complete. + * + * @param info FileInfo to return the file information. + * @return true if the file information is valid, else false + */ + public abstract boolean nextFileInfo(FileInfo info); + + /** + * Return the file name of the next file in the active search. Returns null is the search is + * complete. + * + * @return java.lang.String + */ + public abstract String nextFileName(); + + /** + * Return the total number of file entries for this search if known, else return -1 + * + * @return int + */ + public int numberOfEntries() + { + return -1; + } + + /** + * Restart a search at the specified resume point. + * + * @param resumeId Resume point id. + * @return true if the search can be restarted, else false. + */ + public abstract boolean restartAt(int resumeId); + + /** + * Restart the current search at the specified file. + * + * @param info File to restart the search at. + * @return true if the search can be restarted, else false. + */ + public abstract boolean restartAt(FileInfo info); + + /** + * Set the search context flags. + * + * @param flg int + */ + public final void setFlags(int flg) + { + m_flags = flg; + } + + /** + * Set the maximum files to return per request packet. + * + * @param maxFiles int + */ + public final void setMaximumFiles(int maxFiles) + { + m_maxFiles = maxFiles; + } + + /** + * Set the search string. + * + * @param str java.lang.String + */ + public final void setSearchString(String str) + { + m_searchStr = str; + } + + /** + * Set the tree connection id that the search is associated with. + * + * @param id int + */ + public final void setTreeId(int id) + { + m_treeId = id; + } + + /** + * Return the search context as a string. + * + * @return java.lang.String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + str.append(getSearchString()); + str.append(":"); + str.append(getMaximumFiles()); + str.append(","); + str.append("0x"); + str.append(Integer.toHexString(getFlags())); + str.append("]"); + + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/ShareListener.java b/source/java/org/alfresco/filesys/server/filesys/ShareListener.java new file mode 100644 index 0000000000..8991fbab9f --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/ShareListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.server.SrvSession; + +/** + *

    + * The share listener interface provides a hook into the server so that an application is notified + * when a session connects/disconnects from a particular share. + */ +public interface ShareListener +{ + + /** + * Called when a session connects to a share + * + * @param sess SrvSession + * @param tree TreeConnection + */ + public void shareConnect(SrvSession sess, TreeConnection tree); + + /** + * Called when a session disconnects from a share + * + * @param sess SrvSession + * @param tree TreeConnection + */ + public void shareDisconnect(SrvSession sess, TreeConnection tree); +} diff --git a/source/java/org/alfresco/filesys/server/filesys/SrvDiskInfo.java b/source/java/org/alfresco/filesys/server/filesys/SrvDiskInfo.java new file mode 100644 index 0000000000..067c9ec830 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/SrvDiskInfo.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.smb.PCShare; + +/** + *

    + * The class extends the client side version of the disk information class to allow values to be set + * after construction by a disk interface implementation. + *

    + * The class contains information about the total, free and used blocks on a disk device, and the + * block size and blocks per allocation unit of the device. + */ +public class SrvDiskInfo extends DiskInfo +{ + + /** + * Create an empty disk information object. + */ + public SrvDiskInfo() + { + } + + /** + * Construct a disk information object. + * + * @param totunits int + * @param blkunit int + * @param blksiz int + * @param freeunit int + */ + public SrvDiskInfo(int totunits, int blkunit, int blksiz, int freeunit) + { + super(null, (long) totunits, blkunit, blksiz, (long) freeunit); + } + + /** + * Construct a disk information object. + * + * @param totunits long + * @param blkunit long + * @param blksiz long + * @param freeunit long + */ + public SrvDiskInfo(long totunits, long blkunit, long blksiz, long freeunit) + { + super(null, totunits, (int) blkunit, (int) blksiz, freeunit); + } + + /** + * Class constructor + * + * @param shr PCShare + * @param totunits int + * @param blkunit int + * @param blksiz int + * @param freeunit int + */ + protected SrvDiskInfo(PCShare shr, int totunits, int blkunit, int blksiz, int freeunit) + { + super(shr, totunits, blkunit, blksiz, freeunit); + } + + /** + * Set the block size, in bytes. + * + * @param siz int + */ + public final void setBlockSize(int siz) + { + m_blocksize = siz; + } + + /** + * Set the number of blocks per filesystem allocation unit. + * + * @param blks int + */ + public final void setBlocksPerAllocationUnit(int blks) + { + m_blockperunit = blks; + } + + /** + * Set the number of free units on this shared disk device. + * + * @param units int + */ + public final void setFreeUnits(int units) + { + m_freeunits = units; + } + + /** + * Set the total number of units on this shared disk device. + * + * @param units int + */ + public final void setTotalUnits(int units) + { + m_totalunits = units; + } + + /** + * Set the block size, in bytes. + * + * @param siz long + */ + public final void setBlockSize(long siz) + { + m_blocksize = siz; + } + + /** + * Set the number of blocks per filesystem allocation unit. + * + * @param blks long + */ + public final void setBlocksPerAllocationUnit(long blks) + { + m_blockperunit = blks; + } + + /** + * Set the number of free units on this shared disk device. + * + * @param units long + */ + public final void setFreeUnits(long units) + { + m_freeunits = units; + } + + /** + * Set the total number of units on this shared disk device. + * + * @param units long + */ + public final void setTotalUnits(long units) + { + m_totalunits = units; + } + + /** + * Set the node name. + * + * @param name java.lang.String + */ + protected final void setNodeName(String name) + { + m_nodename = name; + } + + /** + * Set the shared device name. + * + * @param name java.lang.String + */ + protected final void setShareName(String name) + { + m_share = name; + } + + /** + * Copy the disk information details + * + * @param disk SrvDiskInfo + */ + public final void copyFrom(SrvDiskInfo disk) + { + + // Copy the details to this object + + setBlockSize(disk.getBlockSize()); + setBlocksPerAllocationUnit(disk.getBlocksPerAllocationUnit()); + + setFreeUnits(disk.getFreeUnits()); + setTotalUnits(disk.getTotalUnits()); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/TooManyConnectionsException.java b/source/java/org/alfresco/filesys/server/filesys/TooManyConnectionsException.java new file mode 100644 index 0000000000..ce2889ac1d --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/TooManyConnectionsException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * This error indicates that too many tree connections are currently open on a session. The new tree + * connection request will be rejected by the server. + */ +public class TooManyConnectionsException extends Exception +{ + private static final long serialVersionUID = 3257845497929414961L; + + /** + * TooManyConnectionsException constructor. + */ + public TooManyConnectionsException() + { + super(); + } + + /** + * TooManyConnectionsException constructor. + * + * @param s java.lang.String + */ + public TooManyConnectionsException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/TooManyFilesException.java b/source/java/org/alfresco/filesys/server/filesys/TooManyFilesException.java new file mode 100644 index 0000000000..d9991f693b --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/TooManyFilesException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * This error is generated when a tree connection has no free file slots. The new file open request + * will be rejected by the server. + */ +public class TooManyFilesException extends Exception +{ + private static final long serialVersionUID = 4051332218943060273L; + + /** + * TooManyFilesException constructor. + */ + public TooManyFilesException() + { + super(); + } + + /** + * TooManyFilesException constructor. + * + * @param s java.lang.String + */ + public TooManyFilesException(String s) + { + super(s); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/TreeConnection.java b/source/java/org/alfresco/filesys/server/filesys/TreeConnection.java new file mode 100644 index 0000000000..4c84deabf2 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/TreeConnection.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.core.DeviceContext; +import org.alfresco.filesys.server.core.DeviceInterface; +import org.alfresco.filesys.server.core.InvalidDeviceInterfaceException; +import org.alfresco.filesys.server.core.SharedDevice; + +/** + * The tree connection class holds the details of a single SMB tree connection. A tree connection is + * a connection to a shared device. + */ +public class TreeConnection +{ + + // Maximum number of open files allowed per connection. + + public static final int MAXFILES = 8192; + + // Number of initial file slots to allocate. Number of allocated slots will be doubled + // when required until MAXFILES is reached. + + public static final int INITIALFILES = 32; + + // Shared device that the connection is associated with + + private SharedDevice m_shareDev; + + // List of open files on this connection. Count of open file slots used. + + private NetworkFile[] m_files; + private int m_fileCount; + + // Access permission that the user has been granted + + private int m_permission; + + /** + * Construct a tree connection using the specified shared device. + * + * @param shrDev SharedDevice + */ + public TreeConnection(SharedDevice shrDev) + { + m_shareDev = shrDev; + m_shareDev.incrementConnectionCount(); + } + + /** + * Add a network file to the list of open files for this connection. + * + * @param file NetworkFile + * @param sess SrvSession + * @return int + */ + public final int addFile(NetworkFile file, SrvSession sess) throws TooManyFilesException + { + + // Check if the file array has been allocated + + if (m_files == null) + m_files = new NetworkFile[INITIALFILES]; + + // Find a free slot for the network file + + int idx = 0; + + while (idx < m_files.length && m_files[idx] != null) + idx++; + + // Check if we found a free slot + + if (idx == m_files.length) + { + + // The file array needs to be extended, check if we reached the limit. + + if (m_files.length >= MAXFILES) + throw new TooManyFilesException(); + + // Extend the file array + + NetworkFile[] newFiles = new NetworkFile[m_files.length * 2]; + System.arraycopy(m_files, 0, newFiles, 0, m_files.length); + m_files = newFiles; + } + + // Store the network file, update the open file count and return the index + + m_files[idx] = file; + m_fileCount++; + return idx; + } + + /** + * Close the tree connection, release resources. + * + * @param sess SrvSession + */ + public final void closeConnection(SrvSession sess) + { + + // Make sure all files are closed + + if (openFileCount() > 0) + { + + // Close all open files + + for (int idx = 0; idx < m_files.length; idx++) + { + + // Check if the file is active + + if (m_files[idx] != null) + removeFile(idx, sess); + } + } + + // Decrement the active connection count for the shared device + + m_shareDev.decrementConnectionCount(); + } + + /** + * Return the specified network file. + * + * @return NetworkFile + */ + public final NetworkFile findFile(int fid) + { + + // Check if the file id and file array are valid + + if (m_files == null || fid >= m_files.length) + return null; + + // Get the required file details + + return m_files[fid]; + } + + /** + * Return the length of the file table + * + * @return int + */ + public final int getFileTableLength() + { + if (m_files == null) + return 0; + return m_files.length; + } + + /** + * Determine if the shared device has an associated context + * + * @return boolean + */ + public final boolean hasContext() + { + if (m_shareDev != null) + return m_shareDev.getContext() != null ? true : false; + return false; + } + + /** + * Return the interface specific context object. + * + * @return Device interface context object. + */ + public final DeviceContext getContext() + { + if (m_shareDev == null) + return null; + return m_shareDev.getContext(); + } + + /** + * Return the share access permissions that the user has been granted. + * + * @return int + */ + public final int getPermission() + { + return m_permission; + } + + /** + * Deterimine if the access permission for the shared device allows read access + * + * @return boolean + */ + public final boolean hasReadAccess() + { + if (m_permission == FileAccess.ReadOnly || m_permission == FileAccess.Writeable) + return true; + return false; + } + + /** + * Determine if the access permission for the shared device allows write access + * + * @return boolean + */ + public final boolean hasWriteAccess() + { + if (m_permission == FileAccess.Writeable) + return true; + return false; + } + + /** + * Return the shared device that this tree connection is using. + * + * @return SharedDevice + */ + public final SharedDevice getSharedDevice() + { + return m_shareDev; + } + + /** + * Return the shared device interface + * + * @return DeviceInterface + */ + public final DeviceInterface getInterface() + { + if (m_shareDev == null) + return null; + try + { + return m_shareDev.getInterface(); + } + catch (InvalidDeviceInterfaceException ex) + { + } + return null; + } + + /** + * Check if the user has been granted the required access permission for this share. + * + * @param perm int + * @return boolean + */ + public final boolean hasPermission(int perm) + { + if (m_permission >= perm) + return true; + return false; + } + + /** + * Return the count of open files on this tree connection. + * + * @return int + */ + public final int openFileCount() + { + return m_fileCount; + } + + /** + * Remove all files from the tree connection. + */ + public final void removeAllFiles() + { + + // Check if the file array has been allocated + + if (m_files == null) + return; + + // Clear the file list + + for (int idx = 0; idx < m_files.length; m_files[idx++] = null) + ; + m_fileCount = 0; + } + + /** + * Remove a network file from the list of open files for this connection. + * + * @param idx int + * @param sess SrvSession + */ + public final void removeFile(int idx, SrvSession sess) + { + + // Range check the file index + + if (m_files == null || idx >= m_files.length) + return; + + // Make sure the files is closed + + if (m_files[idx] != null && m_files[idx].isClosed() == false) + { + + // Close the file + + try + { + + // Access the disk interface and close the file + + DiskInterface disk = (DiskInterface) m_shareDev.getInterface(); + disk.closeFile(sess, this, m_files[idx]); + m_files[idx].setClosed(true); + } + catch (Exception ex) + { + } + } + + // Remove the file and update the open file count. + + m_files[idx] = null; + m_fileCount--; + } + + /** + * Set the access permission for this share that the user has been granted. + * + * @param perm int + */ + public final void setPermission(int perm) + { + m_permission = perm; + } + + /** + * Return the tree connection as a string. + * + * @return java.lang.String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + str.append(m_shareDev.toString()); + str.append(","); + str.append(m_fileCount); + str.append(":"); + str.append(FileAccess.asString(m_permission)); + str.append("]"); + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/TreeConnectionHash.java b/source/java/org/alfresco/filesys/server/filesys/TreeConnectionHash.java new file mode 100644 index 0000000000..20e138c7d3 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/TreeConnectionHash.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.util.Enumeration; +import java.util.Hashtable; + +/** + * Tree Connection Hash Class + *

    + * Hashtable of TreeConnections for the available disk shared devices. TreeConnections are indexed + * using the hash of the share name to allow mounts to be persistent across server restarts. + */ +public class TreeConnectionHash +{ + + // Share name hash to tree connection + + private Hashtable m_connections; + + /** + * Class constructor + */ + public TreeConnectionHash() + { + m_connections = new Hashtable(); + } + + /** + * Return the number of tree connections in the hash table + * + * @return int + */ + public final int numberOfEntries() + { + return m_connections.size(); + } + + /** + * Add a connection to the list of available connections + * + * @param tree TreeConnection + */ + public final void addConnection(TreeConnection tree) + { + m_connections.put(tree.getSharedDevice().getName().hashCode(), tree); + } + + /** + * Delete a connection from the list + * + * @param shareName String + * @return TreeConnection + */ + public final TreeConnection deleteConnection(String shareName) + { + return (TreeConnection) m_connections.get(shareName.hashCode()); + } + + /** + * Find a connection for the specified share name + * + * @param shareName String + * @return TreeConnection + */ + public final TreeConnection findConnection(String shareName) + { + + // Get the tree connection for the associated share name + + TreeConnection tree = m_connections.get(shareName.hashCode()); + + // Return the tree connection + + return tree; + } + + /** + * Find a connection for the specified share name hash code + * + * @param hashCode int + * @return TreeConnection + */ + public final TreeConnection findConnection(int hashCode) + { + + // Get the tree connection for the associated share name + + TreeConnection tree = m_connections.get(hashCode); + + // Return the tree connection + + return tree; + } + + /** + * Enumerate the connections + * + * @return Enumeration + */ + public final Enumeration enumerateConnections() + { + return m_connections.elements(); + } +} diff --git a/source/java/org/alfresco/filesys/server/filesys/UnsupportedInfoLevelException.java b/source/java/org/alfresco/filesys/server/filesys/UnsupportedInfoLevelException.java new file mode 100644 index 0000000000..aaa5683f40 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/UnsupportedInfoLevelException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +/** + *

    + * This error is generated when a request is made for an information level that is not currently + * supported by the SMB server. + */ +public class UnsupportedInfoLevelException extends Exception +{ + private static final long serialVersionUID = 3762538905790395444L; + + /** + * Class constructor. + */ + public UnsupportedInfoLevelException() + { + super(); + } + + /** + * Class constructor. + * + * @param str java.lang.String + */ + public UnsupportedInfoLevelException(String str) + { + super(str); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/filesys/VolumeInfo.java b/source/java/org/alfresco/filesys/server/filesys/VolumeInfo.java new file mode 100644 index 0000000000..dcb104972a --- /dev/null +++ b/source/java/org/alfresco/filesys/server/filesys/VolumeInfo.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.filesys; + +import java.util.Date; + +/** + * Disk Volume Information Class + */ +public class VolumeInfo +{ + + // Volume label + + private String m_label; + + // Serial number + + private int m_serno = -1; + + // Creation date/time + + private Date m_created; + + /** + * Default constructor + */ + public VolumeInfo() + { + } + + /** + * Class constructor + * + * @param label String + */ + public VolumeInfo(String label) + { + setVolumeLabel(label); + } + + /** + * Class constructor + * + * @param label String + * @param serno int + * @param created Date + */ + public VolumeInfo(String label, int serno, Date created) + { + setVolumeLabel(label); + setSerialNumber(serno); + setCreationDateTime(created); + } + + /** + * Return the volume label + * + * @return String + */ + public final String getVolumeLabel() + { + return m_label; + } + + /** + * Determine if the serial number is valid + * + * @return boolean + */ + public final boolean hasSerialNumber() + { + return m_serno != -1 ? true : false; + } + + /** + * Return the serial number + * + * @return int + */ + public final int getSerialNumber() + { + return m_serno; + } + + /** + * Determine if the creation date/time is valid + * + * @return boolean + */ + public final boolean hasCreationDateTime() + { + return m_created != null ? true : false; + } + + /** + * Return the volume creation date/time + * + * @return Date + */ + public final Date getCreationDateTime() + { + return m_created; + } + + /** + * Set the volume label + * + * @param label + */ + public final void setVolumeLabel(String label) + { + m_label = label; + } + + /** + * Set the serial number + * + * @param serno int + */ + public final void setSerialNumber(int serno) + { + m_serno = serno; + } + + /** + * Set the volume creation date/time + * + * @param created Date + */ + public final void setCreationDateTime(Date created) + { + m_created = created; + } + + /** + * Return the volume information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getVolumeLabel()); + str.append(","); + str.append(getSerialNumber()); + str.append(","); + str.append(getCreationDateTime()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/server/locking/FileLockListener.java b/source/java/org/alfresco/filesys/server/locking/FileLockListener.java new file mode 100644 index 0000000000..3762b2be30 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/locking/FileLockListener.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.locking; + +import org.alfresco.filesys.locking.FileLock; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.filesys.NetworkFile; + +/** + * File Lock Listener Interface. + *

    + * The file lock listener receives events when file locks are granted, released and denied. + */ +public interface FileLockListener +{ + + /** + * Lock has been granted on the specified file. + * + * @param sess SrvSession + * @param file NetworkFile + * @param lock FileLock + */ + void lockGranted(SrvSession sess, NetworkFile file, FileLock lock); + + /** + * Lock has been released on the specified file. + * + * @param sess SrvSession + * @param file NetworkFile + * @param lock FileLock + */ + void lockReleased(SrvSession sess, NetworkFile file, FileLock lock); + + /** + * Lock has been denied on the specified file. + * + * @param sess SrvSession + * @param file NetworkFile + * @param lock FileLock + */ + void lockDenied(SrvSession sess, NetworkFile file, FileLock lock); +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/locking/FileLockingInterface.java b/source/java/org/alfresco/filesys/server/locking/FileLockingInterface.java new file mode 100644 index 0000000000..817aaaa493 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/locking/FileLockingInterface.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.locking; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.filesys.TreeConnection; + +/** + * File Locking Interface + *

    + * Optional interface that a DiskInterface driver can implement to provide file locking support. + */ +public interface FileLockingInterface +{ + + /** + * Return the lock manager implementation associated with this virtual filesystem + * + * @param sess SrvSession + * @param tree TreeConnection + * @return LockManager + */ + public LockManager getLockManager(SrvSession sess, TreeConnection tree); +} diff --git a/source/java/org/alfresco/filesys/server/locking/LockManager.java b/source/java/org/alfresco/filesys/server/locking/LockManager.java new file mode 100644 index 0000000000..4c12821840 --- /dev/null +++ b/source/java/org/alfresco/filesys/server/locking/LockManager.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.server.locking; + +import java.io.IOException; + +import org.alfresco.filesys.locking.FileLock; +import org.alfresco.filesys.locking.LockConflictException; +import org.alfresco.filesys.locking.NotLockedException; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.TreeConnection; + +/** + * Lock Manager Interface + *

    + * A lock manager implementation provides file locking support for a virtual filesystem. + */ +public interface LockManager +{ + + /** + * Lock a byte range within a file, or the whole file. + * + * @param sess SrvSession + * @param tree TreeConnection + * @param file NetworkFile + * @param lock FileLock + * @exception LockConflictException + * @exception IOException + */ + public void lockFile(SrvSession sess, TreeConnection tree, NetworkFile file, FileLock lock) + throws LockConflictException, IOException; + + /** + * Unlock a byte range within a file, or the whole file + * + * @param sess SrvSession + * @param tree TreeConnection + * @param file NetworkFile + * @param lock FileLock + * @exception NotLockedException + * @exception IOException + */ + public void unlockFile(SrvSession sess, TreeConnection tree, NetworkFile file, FileLock lock) + throws NotLockedException, IOException; + + /** + * Create a lock object, allows the FileLock object to be extended + * + * @param sess SrvSession + * @param tree TreeConnection + * @param file NetworkFile + * @param offset long + * @param len long + * @param pid int + * @return FileLock + */ + public FileLock createLockObject(SrvSession sess, TreeConnection tree, NetworkFile file, long offset, long len, + int pid); + + /** + * Release all locks that a session has on a file. This method is called to perform cleanup if a + * file is closed that has active locks or if a session abnormally terminates. + * + * @param sess SrvSession + * @param tree TreeConnection + * @param file NetworkFile + */ + public void releaseLocksForFile(SrvSession sess, TreeConnection tree, NetworkFile file); +} diff --git a/source/java/org/alfresco/filesys/smb/Capability.java b/source/java/org/alfresco/filesys/smb/Capability.java new file mode 100644 index 0000000000..83b7ec8652 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/Capability.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB Capabilities Class + *

    + * Contains the capability flags for the client/server during a session setup. + * + * @author GKSpencer + */ +public class Capability +{ + // Capabilities + + public static final int RawMode = 0x00000001; + public static final int MpxMode = 0x00000002; + public static final int Unicode = 0x00000004; + public static final int LargeFiles = 0x00000008; + public static final int NTSMBs = 0x00000010; + public static final int RemoteAPIs = 0x00000020; + public static final int NTStatus = 0x00000040; + public static final int Level2Oplocks = 0x00000080; + public static final int LockAndRead = 0x00000100; + public static final int NTFind = 0x00000200; + public static final int DFS = 0x00001000; + public static final int InfoPassthru = 0x00002000; + public static final int LargeRead = 0x00004000; + public static final int LargeWrite = 0x00008000; + public static final int UnixExtensions = 0x00800000; + public static final int BulkTransfer = 0x20000000; + public static final int CompressedData = 0x40000000; + public static final int ExtendedSecurity = 0x80000000; +} diff --git a/source/java/org/alfresco/filesys/smb/DataType.java b/source/java/org/alfresco/filesys/smb/DataType.java new file mode 100644 index 0000000000..e4a988752a --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/DataType.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB data type class. + *

    + * This class contains the data types that are used within an SMB protocol packet. + */ + +public class DataType +{ + + // SMB data types + + public static final char DataBlock = (char) 0x01; + public static final char Dialect = (char) 0x02; + public static final char Pathname = (char) 0x03; + public static final char ASCII = (char) 0x04; + public static final char VariableBlock = (char) 0x05; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/Dialect.java b/source/java/org/alfresco/filesys/smb/Dialect.java new file mode 100644 index 0000000000..984be3d5ff --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/Dialect.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB dialect class. + *

    + * This class contains the available SMB protocol dialects that may be negotiated when an SMB + * session is setup. + */ + +public final class Dialect +{ + + // SMB dialect strings, encoded into the SMB session setup packet. + + private static final String[] protList = { + "PC NETWORK PROGRAM 1.0", + "MICROSOFT NETWORKS 1.03", + "MICROSOFT NETWORKS 3.0", + "DOS LANMAN1.0", + "LANMAN1.0", + "DOS LM1.2X002", + "LM1.2X002", + "DOS LANMAN2.1", + "LANMAN2.1", + "Samba", + "NT LM 0.12", + "NT LANMAN 1.0" }; + + // SMB dialect type strings + + private static final String[] protType = { + "Core", + "CorePlus", + "DOS LANMAN 1.0", + "LANMAN1.0", + "DOS LANMAN 2.1", + "LM1.2X002", + "LANMAN2.1", + "NT LM 0.12" }; + + // Dialect constants + + public static final int Core = 0; + public static final int CorePlus = 1; + public static final int DOSLanMan1 = 2; + public static final int LanMan1 = 3; + public static final int DOSLanMan2 = 4; + public static final int LanMan2 = 5; + public static final int LanMan2_1 = 6; + public static final int NT = 7; + public static final int Max = 8; + + public static final int Unknown = -1; + + // SMB dialect type to string conversion array + + private static final int[] protIdx = { + Core, + CorePlus, + DOSLanMan1, + DOSLanMan1, + LanMan1, + DOSLanMan2, + LanMan2, + LanMan2_1, + LanMan2_1, + NT, + NT, + NT }; + + // SMB dialect type to string conversion array length + + public static final int SMB_PROT_MAXSTRING = protIdx.length; + + // Table that maps SMB commands to the minimum required SMB dialect + + private static final int[] cmdtable = { + Core, // CreateDirectory + Core, // DeleteDirectory + Core, // OpenFile + Core, // CreateFile + Core, // CloseFile + Core, // FlushFile + Core, // DeleteFile + Core, // RenameFile + Core, // QueryFileInfo + Core, // SetFileInfo + Core, // Read + Core, // Write + Core, // LockFile + Core, // UnlockFile + Core, // CreateTemporary + Core, // CreateNew + Core, // CheckDirectory + Core, // ProcessExit + Core, // SeekFile + LanMan1, // LockAndRead + LanMan1, // WriteAndUnlock + 0, // Unused + 0, // .. + 0, // .. + 0, // .. + 0, // .. + LanMan1, // ReadRaw + LanMan1, // WriteMpxSecondary + LanMan1, // WriteRaw + LanMan1, // WriteMpx + 0, // Unused + LanMan1, // WriteComplete + 0, // Unused + LanMan1, // SetInformation2 + LanMan1, // QueryInformation2 + LanMan1, // LockingAndX + LanMan1, // Transaction + LanMan1, // TransactionSecondary + LanMan1, // Ioctl + LanMan1, // Ioctl2 + LanMan1, // Copy + LanMan1, // Move + LanMan1, // Echo + LanMan1, // WriteAndClose + LanMan1, // OpenAndX + LanMan1, // ReadAndX + LanMan1, // WriteAndX + 0, // Unused + LanMan1, // CloseAndTreeDisconnect + LanMan2, // Transaction2 + LanMan2, // Transaction2Secondary + LanMan2, // FindClose2 + LanMan1, // FindNotifyClose + 0, // Unused + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + Core, // TreeConnect + Core, // TreeDisconnect + Core, // Negotiate + Core, // SessionSetupAndX + LanMan1, // LogoffAndX + LanMan1, // TreeConnectAndX + 0, // Unused + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + Core, // DiskInformation + Core, // Search + LanMan1, // Find + LanMan1, // FindUnique + 0, // Unused + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + NT, // NTTransact + NT, // NTTransactSecondary + NT, // NTCreateAndX + NT, // NTCancel + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + Core, // OpenPrintFile + Core, // WritePrintFile + Core, // ClosePrintFile + Core, // GetPrintQueue + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + 0, // .. + -1, // SendMessage + -1, // SendBroadcast + -1, // SendForward + -1, // CancelForward + -1, // GetMachineName + -1, // SendMultiStart + -1, // SendMultiEnd + -1 // SendMultiText + }; + + /** + * Return the required SMB dialect string. + * + * @param i SMB dialect string index. + * @return SMB dialect string. + */ + + public static String DialectString(int i) + { + + // Validate the dialect index + + if (i >= protList.length) + return null; + return protList[i]; + } + + /** + * Determine if the SMB dialect supports the SMB command + * + * @return boolean + * @param dialect int SMB dialect type. + * @param cmd int SMB command code. + */ + public final static boolean DialectSupportsCommand(int dialect, int cmd) + { + // Range check the command + + if (cmd > cmdtable.length) + return false; + + // Check if the SMB dialect supports the SMB command. + + if (cmdtable[cmd] <= dialect) + return true; + return false; + } + + /** + * Return the SMB dialect type for the specified SMB dialect string index. + * + * @param i SMB dialect type. + * @return SMB dialect string index. + */ + + public static int DialectType(int i) + { + return protIdx[i]; + } + + /** + * Return the SMB dialect type for the specified string. + * + * @return int + * @param diastr java.lang.String + */ + public static int DialectType(String diastr) + { + + // Search the protocol string list + + int i = 0; + + while (i < protList.length && protList[i].compareTo(diastr) != 0) + i++; + + // Return the protocol id + + if (i < protList.length) + return DialectType(i); + else + return Unknown; + } + + /** + * Return the dialect type as a string. + * + * @param dia SMB dialect type. + * @return SMB dialect type string. + */ + + public static String DialectTypeString(int dia) + { + return protType[dia]; + } + + /** + * Return the number of available SMB dialect strings. + * + * @return Number of available SMB dialect strings. + */ + + public static int NumberOfDialects() + { + return protList.length; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/DialectSelector.java b/source/java/org/alfresco/filesys/smb/DialectSelector.java new file mode 100644 index 0000000000..332ed08d31 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/DialectSelector.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +import java.util.BitSet; + +/** + * SMB dialect selector class. + */ +public class DialectSelector +{ + + // Bit set of selected SMB dialects. + + private BitSet dialects; + + /** + * Construct a new SMB dialect selector with the SMB core protocol selected. + */ + + public DialectSelector() + { + dialects = new BitSet(Dialect.Max); + + // Select only the core protocol by default + + ClearAll(); + AddDialect(Dialect.Core); + } + + /** + * Add a dialect to the list of available SMB dialects. + * + * @param idx Index of the dialect to add to the available dialects. + * @exception java.lang.ArrayIndexOutOfBoundsException Invalid dialect index. + */ + + public void AddDialect(int d) throws java.lang.ArrayIndexOutOfBoundsException + { + dialects.set(d); + } + + /** + * Clear all the dialect bits. + */ + + public void ClearAll() + { + for (int i = 0; i < dialects.size(); dialects.clear(i++)) + ; + } + + /** + * Copy the SMB dialect selector settings. + * + * @param dsel DialectSelector + */ + public void copyFrom(DialectSelector dsel) + { + + // Clear all current settings + + ClearAll(); + + // Copy the settings + + for (int i = 0; i < Dialect.Max; i++) + { + + // Check if the dialect is enabled + + if (dsel.hasDialect(i)) + AddDialect(i); + } + } + + /** + * Determine if the specified SMB dialect is selected/available. + * + * @param idx Index of the dialect to test for. + * @return true if the SMB dialect is available, else false. + * @exception java.lang.ArrayIndexOutOfBoundsException Invalid dialect index. + */ + + public boolean hasDialect(int d) throws java.lang.ArrayIndexOutOfBoundsException + { + return dialects.get(d); + } + + /** + * Determine if the core SMB dialect is enabled + * + * @return boolean + */ + public boolean hasCore() + { + if (hasDialect(Dialect.Core) || hasDialect(Dialect.CorePlus)) + return true; + return false; + } + + /** + * Determine if the LanMan SMB dialect is enabled + * + * @return boolean + */ + public boolean hasLanMan() + { + if (hasDialect(Dialect.DOSLanMan1) || hasDialect(Dialect.DOSLanMan2) || hasDialect(Dialect.LanMan1) + || hasDialect(Dialect.LanMan2) || hasDialect(Dialect.LanMan2_1)) + return true; + return false; + } + + /** + * Determine if the NT SMB dialect is enabled + * + * @return boolean + */ + public boolean hasNT() + { + if (hasDialect(Dialect.NT)) + return true; + return false; + } + + /** + * Remove an SMB dialect from the list of available dialects. + * + * @param idx Index of the dialect to remove. + * @exception java.lang.ArrayIndexOutOfBoundsException Invalid dialect index. + */ + + public void RemoveDialect(int d) throws java.lang.ArrayIndexOutOfBoundsException + { + dialects.clear(d); + } + + /** + * Return the dialect selector list as a string. + * + * @return java.lang.String + */ + public String toString() + { + + // Create a string buffer to build the return string + + StringBuffer str = new StringBuffer(); + str.append("["); + + for (int i = 0; i < dialects.size(); i++) + { + if (hasDialect(i)) + { + str.append(Dialect.DialectTypeString(i)); + str.append(","); + } + } + + // Trim the last comma and return the string + + if (str.length() > 1) + str.setLength(str.length() - 1); + str.append("]"); + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/DirectoryWatcher.java b/source/java/org/alfresco/filesys/smb/DirectoryWatcher.java new file mode 100644 index 0000000000..b433d3b322 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/DirectoryWatcher.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Directory Watcher Interface + */ +public interface DirectoryWatcher +{ + + // Notification event types + + public final static int FileActionUnknown = -1; + public final static int FileNoAction = 0; + public final static int FileAdded = 1; + public final static int FileRemoved = 2; + public final static int FileModified = 3; + public final static int FileRenamedOld = 4; + public final static int FileRenamedNew = 5; + + /** + * Directory change occurred + * + * @param typ int + * @param fname String + */ + public void directoryChanged(int typ, String fname); +} diff --git a/source/java/org/alfresco/filesys/smb/FileInfoLevel.java b/source/java/org/alfresco/filesys/smb/FileInfoLevel.java new file mode 100644 index 0000000000..d8f023c282 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/FileInfoLevel.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * File Information Levels class. This class contains the file information levels that may be + * requested in the various Transact2 requests. + */ +public class FileInfoLevel +{ + + // Find first/next information levels + + public static final int FindStandard = 0x0001; + public static final int FindQueryEASize = 0x0002; + public static final int FindQueryEAsList = 0x0003; + public static final int FindFileDirectory = 0x0101; + public static final int FindFileFullDirectory = 0x0102; + public static final int FindFileNames = 0x0103; + public static final int FindFileBothDirectory = 0x0104; + + // File information levels + + public static final int SetStandard = 0x0001; + public static final int SetQueryEASize = 0x0002; + public static final int SetBasicInfo = 0x0101; + public static final int SetDispositionInfo = 0x0102; + public static final int SetAllocationInfo = 0x0103; + public static final int SetEndOfFileInfo = 0x0104; + + // Query path information levels + + public static final int PathStandard = 0x0001; + public static final int PathQueryEASize = 0x0002; + public static final int PathQueryEAsFromList = 0x0003; + public static final int PathAllEAs = 0x0004; + public static final int PathIsNameValid = 0x0006; + public static final int PathFileBasicInfo = 0x0101; + public static final int PathFileStandardInfo = 0x0102; + public static final int PathFileEAInfo = 0x0103; + public static final int PathFileNameInfo = 0x0104; + public static final int PathFileAllInfo = 0x0107; + public static final int PathFileAltNameInfo = 0x0108; + public static final int PathFileStreamInfo = 0x0109; + public static final int PathFileCompressionInfo = 0x010B; + + // Filesystem query information levels + + public static final int FSInfoAllocation = 0x0001; + public static final int FSInfoVolume = 0x0002; + public static final int FSInfoQueryVolume = 0x0102; + public static final int FSInfoQuerySize = 0x0103; + public static final int FSInfoQueryDevice = 0x0104; + public static final int FSInfoQueryAttribute = 0x0105; + + // NT pasthru levels + + public static final int NTFileBasicInfo = 1004; + public static final int NTFileStandardInfo = 1005; + public static final int NTFileInternalInfo = 1006; + public static final int NTFileEAInfo = 1007; + public static final int NTFileAccessInfo = 1008; + public static final int NTFileNameInfo = 1009; + public static final int NTFileRenameInfo = 1010; + public static final int NTFileDispositionInfo = 1013; + public static final int NTFilePositionInfo = 1014; + public static final int NTFileModeInfo = 1016; + public static final int NTFileAlignmentInfo = 1017; + public static final int NTFileAllInfo = 1018; + public static final int NTFileAltNameInfo = 1021; + public static final int NTFileStreamInfo = 1022; + public static final int NTFileCompressionInfo = 1028; + public static final int NTNetworkOpenInfo = 1034; + public static final int NTAttributeTagInfo = 1035; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/FindFirstNext.java b/source/java/org/alfresco/filesys/smb/FindFirstNext.java new file mode 100644 index 0000000000..0002bbc60b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/FindFirstNext.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Find First/Next Flags + */ +public class FindFirstNext +{ + // Find first/find next flags + + public static final int CloseSearch = 0x01; + public static final int CloseAtEnd = 0x02; + public static final int ReturnResumeKey = 0x04; + public static final int ResumePrevious = 0x08; + public static final int BackupIntent = 0x10; +} diff --git a/source/java/org/alfresco/filesys/smb/InvalidUNCPathException.java b/source/java/org/alfresco/filesys/smb/InvalidUNCPathException.java new file mode 100644 index 0000000000..0b31ce845d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/InvalidUNCPathException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Invalid UNC path exception class + *

    + * The InvalidUNCPathException indicates that a UNC path has an invalid format. + * + * @see PCShare + */ +public class InvalidUNCPathException extends Exception +{ + private static final long serialVersionUID = 3257567304241066297L; + + /** + * Default invalid UNC path exception constructor. + */ + + public InvalidUNCPathException() + { + } + + /** + * Invalid UNC path exception constructor, with additional details string. + */ + + public InvalidUNCPathException(String msg) + { + super(msg); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/LockingAndX.java b/source/java/org/alfresco/filesys/smb/LockingAndX.java new file mode 100644 index 0000000000..76a652765e --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/LockingAndX.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * LockingAndX SMB Constants Class + */ +public class LockingAndX +{ + + // Lock type flags + + public static final int SharedLock = 0x0001; + public static final int OplockBreak = 0x0002; + public static final int ChangeType = 0x0004; + public static final int Cancel = 0x0008; + public static final int LargeFiles = 0x0010; + + /** + * Check if this is a normal lock/unlock, ie. no flags except the LargeFiles flag may be set + * + * @param flags + * @return boolean + */ + public final static boolean isNormalLockUnlock(int flags) + { + return (flags & 0x000F) == 0 ? true : false; + } + + /** + * Check if the large files flag is set + * + * @param flags int + * @return boolean + */ + public final static boolean hasLargeFiles(int flags) + { + return (flags & LargeFiles) != 0 ? true : false; + } + + /** + * Check if the shared lock flag is set + * + * @param flags int + * @return boolean + */ + public final static boolean hasSharedLock(int flags) + { + return (flags & SharedLock) != 0 ? true : false; + } + + /** + * Check if the oplock break flag is set + * + * @param flags int + * @return boolean + */ + public final static boolean hasOplockBreak(int flags) + { + return (flags & OplockBreak) != 0 ? true : false; + } + + /** + * Check if the change type flag is set + * + * @param flags int + * @return boolean + */ + public final static boolean hasChangeType(int flags) + { + return (flags & ChangeType) != 0 ? true : false; + } + + /** + * Check if the cancel flag is set + * + * @param flags int + * @return boolean + */ + public final static boolean hasCancel(int flags) + { + return (flags & Cancel) != 0 ? true : false; + } +} diff --git a/source/java/org/alfresco/filesys/smb/NTIOCtl.java b/source/java/org/alfresco/filesys/smb/NTIOCtl.java new file mode 100644 index 0000000000..2b773b2d17 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/NTIOCtl.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * NT IO Control Codes Class + */ +public class NTIOCtl +{ + + // Device type codes + + public static final int DeviceBeep = 0x0001; + public static final int DeviceCDRom = 0x0002; + public static final int DeviceCDRomFileSystem = 0x0003; + public static final int DeviceController = 0x0004; + public static final int DeviceDataLink = 0x0005; + public static final int DeviceDFS = 0x0006; + public static final int DeviceDisk = 0x0007; + public static final int DeviceDiskFileSystem = 0x0008; + public static final int DeviceFileSystem = 0x0009; + public static final int DeviceInportPort = 0x000A; + public static final int DeviceKeyboard = 0x000B; + public static final int DeviceMailSlot = 0x000C; + public static final int DeviceMidiIn = 0x000D; + public static final int DeviceMidiOut = 0x000E; + public static final int DeviceMouse = 0x000F; + public static final int DeviceMultiUNCProvider = 0x0010; + public static final int DeviceNamedPipe = 0x0011; + public static final int DeviceNetwork = 0x0012; + public static final int DeviceNetworkBrowser = 0x0013; + public static final int DeviceNetworkFileSystem = 0x0014; + public static final int DeviceNull = 0x0015; + public static final int DeviceParallelPort = 0x0016; + public static final int DevicePhysicalNetCard = 0x0017; + public static final int DevicePrinter = 0x0018; + public static final int DeviceScanner = 0x0019; + public static final int DeviceSerialMousePort = 0x001A; + public static final int DeviceSerialPort = 0x001B; + public static final int DeviceScreen = 0x001C; + public static final int DeviceSound = 0x001D; + public static final int DeviceStreams = 0x001E; + public static final int DeviceTape = 0x001F; + public static final int DeviceTapeFileSystem = 0x0020; + public static final int DeviceTransport = 0x0021; + public static final int DeviceUnknown = 0x0022; + public static final int DeviceVideo = 0x0023; + public static final int DeviceVirtualDisk = 0x0024; + public static final int DeviceWaveIn = 0x0025; + public static final int DeviceWaveOut = 0x0026; + public static final int Device8042Port = 0x0027; + public static final int DeviceNetworkRedirector = 0x0028; + public static final int DeviceBattery = 0x0029; + public static final int DeviceBusExtender = 0x002A; + public static final int DeviceModem = 0x002B; + public static final int DeviceVDM = 0x002C; + public static final int DeviceMassStorage = 0x002D; + public static final int DeviceSMB = 0x002E; + public static final int DeviceKS = 0x002F; + public static final int DeviceChanger = 0x0030; + public static final int DeviceSmartCard = 0x0031; + public static final int DeviceACPI = 0x0032; + public static final int DeviceDVD = 0x0033; + public static final int DeviceFullScreenVideo = 0x0034; + public static final int DeviceDFSFileSystem = 0x0035; + public static final int DeviceDFSVolume = 0x0036; + + // Method types for I/O and filesystem controls + + public static final int MethodBuffered = 0; + public static final int MethodInDirect = 1; + public static final int MethodOutDirect = 2; + public static final int MethodNeither = 3; + + // Access check types + + public static final int AccessAny = 0; + public static final int AccessRead = 0x0001; + public static final int AccessWrite = 0x0002; + + // Filesystem function codes + + public static final int FsCtlRequestOplockLevel1 = 0; + public static final int FsCtlRequestOplockLevel2 = 1; + public static final int FsCtlRequestBatchOplock = 2; + public static final int FsCtlOplockBreakAck = 3; + public static final int FsCtlOpBatchAckClosePend = 4; + public static final int FsCtlOplockBreakNotify = 5; + public static final int FsCtlLockVolume = 6; + public static final int FsCtlUnlockVolume = 7; + public static final int FsCtlDismountVolume = 8; + public static final int FsCtlIsVolumeMounted = 10; + public static final int FsCtlIsPathnameValid = 11; + public static final int FsCtlMarkVolumeDirty = 12; + public static final int FsCtlQueryRetrievalPtrs = 14; + public static final int FsCtlGetCompression = 15; + public static final int FsCtlSetCompression = 16; + public static final int FsCtlMarkAsSystemHive = 19; + public static final int FsCtlOplockBreakAck2 = 20; + public static final int FsCtlInvalidateVolumes = 21; + public static final int FsCtlQueryFatBPB = 22; + public static final int FsCtlRequestFilterOplock = 23; + public static final int FsCtlFileSysGetStats = 24; + public static final int FsCtlGetNTFSVolumeData = 25; + public static final int FsCtlGetNTFSFileRecord = 26; + public static final int FsCtlGetVolumeBitmap = 27; + public static final int FsCtlGetRetrievalPtrs = 28; + public static final int FsCtlMoveFile = 29; + public static final int FsCtlIsVolumeDirty = 30; + public static final int FsCtlGetHFSInfo = 31; + public static final int FsCtlAllowExtenDasdIO = 32; + public static final int FsCtlReadPropertyData = 33; + public static final int FsCtlWritePropertyData = 34; + public static final int FsCtlFindFilesBySID = 35; + public static final int FsCtlDumpPropertyData = 37; + public static final int FsCtlSetObjectId = 38; + public static final int FsCtlGetObjectId = 39; + public static final int FsCtlDeleteObjectId = 40; + public static final int FsCtlSetReparsePoint = 41; + public static final int FsCtlGetReparsePoint = 42; + public static final int FsCtlDeleteReparsePoint = 43; + public static final int FsCtlEnumUsnData = 44; + public static final int FsCtlSecurityIdCheck = 45; + public static final int FsCtlReadUsnJournal = 46; + public static final int FsCtlSetObjectIdExtended = 47; + public static final int FsCtlCreateOrGetObjectId = 48; + public static final int FsCtlSetSparse = 49; + public static final int FsCtlSetZeroData = 50; + public static final int FsCtlQueryAllocRanges = 51; + public static final int FsCtlEnableUpgrade = 52; + public static final int FsCtlSetEncryption = 53; + public static final int FsCtlEncryptionFsCtlIO = 54; + public static final int FsCtlWriteRawEncrypted = 55; + public static final int FsCtlReadRawEncrypted = 56; + public static final int FsCtlCreateUsnJournal = 57; + public static final int FsCtlReadFileUsnData = 58; + public static final int FsCtlWriteUsnCloseRecord = 59; + public static final int FsCtlExtendVolume = 60; + + /** + * Extract the device type from an I/O control code + * + * @param ioctl int + * @return int + */ + public final static int getDeviceType(int ioctl) + { + return (ioctl >> 16) & 0x0000FFFF; + } + + /** + * Extract the access type from an I/O control code + * + * @param ioctl int + * @return int + */ + public final static int getAccessType(int ioctl) + { + return (ioctl >> 14) & 0x00000003; + } + + /** + * Extract the function code from the I/O control code + * + * @param ioctl int + * @return int + */ + public final static int getFunctionCode(int ioctl) + { + return (ioctl >> 2) & 0x00000FFF; + } + + /** + * Extract the method code from the I/O control code + * + * @param ioctl int + * @return int + */ + public final static int getMethod(int ioctl) + { + return ioctl & 0x00000003; + } + + /** + * Make a control code + * + * @param devType int + * @param func int + * @param method int + * @param access int + * @return int + */ + public final static int makeControlCode(int devType, int func, int method, int access) + { + return (devType << 16) + (access << 14) + (func << 2) + (method); + } + + /** + * Return an I/O control code as a string + * + * @param ioctl int + * @return String + */ + public final static String asString(int ioctl) + { + StringBuffer str = new StringBuffer(); + + str.append("[Func:"); + str.append(getFunctionCode(ioctl)); + + str.append(",DevType:"); + str.append(getDeviceType(ioctl)); + + str.append(",Access:"); + str.append(getAccessType(ioctl)); + + str.append(",Method:"); + str.append(getMethod(ioctl)); + + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/NTTime.java b/source/java/org/alfresco/filesys/smb/NTTime.java new file mode 100644 index 0000000000..970c611a9c --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/NTTime.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +import java.util.Date; + +/** + * NT 64bit Time Conversion Class + *

    + * Convert an NT 64bit time value to a Java Date value and vice versa. + */ +public class NTTime +{ + // NT time value indicating infinite time + + public static final long InfiniteTime = 0x7FFFFFFFFFFFFFFFL; + + // Time conversion constant, difference between NT 64bit base date of 1-1-1601 00:00:00 and + // Java base date of 1-1-1970 00:00:00. In 100ns units. + + private static final long TIME_CONVERSION = 116444736000000000L; + + /** + * Convert a Java Date value to an NT 64bit time + * + * @param jdate Date + * @return long + */ + public final static long toNTTime(Date jdate) + { + + // Add the conversion constant to the Java date raw value, convert the Java milliseconds to + // 100ns units + + long ntDate = (jdate.getTime() * 10000L) + TIME_CONVERSION; + return ntDate; + } + + /** + * Convert a Java Date value to an NT 64bit time + * + * @param jdate long + * @return long + */ + public final static long toNTTime(long jdate) + { + + // Add the conversion constant to the Java date raw value, convert the Java milliseconds to + // 100ns units + + long ntDate = (jdate * 10000L) + TIME_CONVERSION; + return ntDate; + } + + /** + * Convert an NT 64bit time value to a Java date value + * + * @param ntDate long + * @return SMBDate + */ + public final static SMBDate toSMBDate(long ntDate) + { + + // Convert the NT 64bit 100ns time value to a Java milliseconds value + + long jDate = (ntDate - TIME_CONVERSION) / 10000L; + return new SMBDate(jDate); + } + + /** + * Convert an NT 64bit time value to a Java date value + * + * @param ntDate long + * @return long + */ + public final static long toJavaDate(long ntDate) + { + + // Convert the NT 64bit 100ns time value to a Java milliseconds value + + long jDate = (ntDate - TIME_CONVERSION) / 10000L; + return jDate; + } +} diff --git a/source/java/org/alfresco/filesys/smb/NetworkSession.java b/source/java/org/alfresco/filesys/smb/NetworkSession.java new file mode 100644 index 0000000000..0e85f38735 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/NetworkSession.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +import java.io.IOException; +import java.net.UnknownHostException; + +/** + * Network Session Interface + *

    + * Base class for client network sessions. + */ +public interface NetworkSession +{ + + /** + * Return the protocol name + * + * @return String + */ + public String getProtocolName(); + + /** + * Open a connection to a remote host + * + * @param toName Host name/address being called + * @param fromName Local host name/address + * @param toAddr Optional address of the remote host + * @exception IOException + * @exception UnknownHostException + */ + public void Open(String toName, String fromName, String toAddr) throws IOException, UnknownHostException; + + /** + * Determine if the session is connected to a remote host + * + * @return boolean + */ + public boolean isConnected(); + + /** + * Check if the network session has data available + * + * @return boolean + * @exception IOException + */ + public boolean hasData() throws IOException; + + /** + * Receive a data packet from the remote host. + * + * @param buf Byte buffer to receive the data into. + * @param tmo Receive timeout in milliseconds, or zero for no timeout + * @return Length of the received data. + * @exception java.io.IOException I/O error occurred. + */ + public int Receive(byte[] buf, int tmo) throws IOException; + + /** + * Send a data packet to the remote host. + * + * @param data Byte array containing the data to be sent. + * @param siz Length of the data to send. + * @return true if the data was sent successfully, else false. + * @exception java.io.IOException I/O error occurred. + */ + public boolean Send(byte[] data, int siz) throws IOException; + + /** + * Close the network session + * + * @exception java.io.IOException I/O error occurred + */ + public void Close() throws IOException; +} diff --git a/source/java/org/alfresco/filesys/smb/PCShare.java b/source/java/org/alfresco/filesys/smb/PCShare.java new file mode 100644 index 0000000000..4abe8ea9f8 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/PCShare.java @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * PC share class. + *

    + * The PC share class holds the details of a network share, including the required username and + * password access control. + */ +public final class PCShare +{ + + // Domain name + + private String m_domain = null; + + // Node name string. + + private String m_nodename = null; + + // Remote share name string. + + private String m_shrname = null; + + // User name access control string. + + private String m_username = null; + + // Password access control string. + + private String m_password = null; + + // Remote path, relative to the share. + + private String m_path = null; + + // File name string. + + private String m_fname = null; + + // Primary and secondary protocols to try connection on + + private int m_primaryProto = Protocol.UseDefault; + private int m_secondaryProto = Protocol.UseDefault; + + // Extended security negotiation flags + + private int m_extendedSecFlags; + + /** + * Construct an empty PCShare object. + */ + public PCShare() + { + } + + /** + * Construct a PCShare using the supplied UNC path. + * + * @param netpath Network path of the remote server, in UNC format ie. \\node\\share. + * @exception InvalidUNCPathException If the network path is invalid. + */ + public PCShare(String netpath) throws InvalidUNCPathException + { + setNetworkPath(netpath); + + // If the user name has not been set, use the guest account + + if (m_username == null) + setUserName("GUEST"); + } + + /** + * Construct a PCShare using the specified remote server and access control details. + * + * @param nname Node name of the remote server. + * @param shr Share name on the remote server. + * @param uname User name used to access the remote share. + * @param pwd Password used to access the remote share. + */ + public PCShare(String nname, String shr, String uname, String pwd) + { + setNodeName(nname); + setShareName(shr); + setUserName(uname); + setPassword(pwd); + } + + /** + * Build a share relative path using the supplied working directory and file name. + * + * @param workdir Working directory string, relative to the root of the share. + * @param fname File name string. + * @return Share relative path string. + */ + public static String makePath(String workdir, String fname) + { + + // Create a string buffer to build the share relative path + + StringBuffer pathStr = new StringBuffer(); + + // Make sure there is a leading '\' on the path string + + if (!workdir.startsWith("\\")) + pathStr.append("\\"); + pathStr.append(workdir); + + // Make sure the path ends with '\' + + if (pathStr.charAt(pathStr.length() - 1) != '\\') + pathStr.append("\\"); + + // Add the file name to the path string + + pathStr.append(fname); + + // Return share relative the path string + + return pathStr.toString(); + } + + /** + * Return the domain for the share. + * + * @return java.lang.String + */ + public final String getDomain() + { + return m_domain; + } + + /** + * Determine if extended security flags have been set + * + * @return boolean + */ + public final boolean hasExtendedSecurityFlags() + { + return m_extendedSecFlags != 0 ? true : false; + } + + /** + * Return the extended security flags + * + * @return int + */ + public final int getExtendedSecurityFlags() + { + return m_extendedSecFlags; + } + + /** + * Get the remote file name string. + * + * @return Remote file name string. + */ + public final String getFileName() + { + return m_fname; + } + + /** + * Return the full UNC path for this PC share object. + * + * @return Path string of the remote share/path/file in UNC format, ie. \\node\share\path\file. + */ + public final String getNetworkPath() + { + + // Create a string buffer to build up the full network path + + StringBuffer strBuf = new StringBuffer(128); + + // Add the node name and share name + + strBuf.append("\\\\"); + strBuf.append(getNodeName()); + strBuf.append("\\"); + strBuf.append(getShareName()); + + // Add the path, if there is one + + if (getPath() != null && getPath().length() > 0) + { + if (getPath().charAt(0) != '\\') + { + strBuf.append("\\"); + } + strBuf.append(getPath()); + } + + // Add the file name if there is one + + if (getFileName() != null && getFileName().length() > 0) + { + if (strBuf.charAt(strBuf.length() - 1) != '\\') + { + strBuf.append("\\"); + } + strBuf.append(getFileName()); + } + + // Return the network path + + return strBuf.toString(); + } + + /** + * Get the remote node name string. + * + * @return Node name string. + */ + public final String getNodeName() + { + return m_nodename; + } + + /** + * Get the remote password required to access the remote share. + * + * @return Remote password string. + */ + public final String getPassword() + { + return m_password; + } + + /** + * Get the share relative path string. + * + * @return Share relative path string. + */ + public final String getPath() + { + return m_path != null ? m_path : "\\"; + } + + /** + * Return the share relative path for this PC share object. + * + * @return Path string of the remote share/path/file relative to the share, ie. \path\file. + */ + public final String getRelativePath() + { + + // Create a string buffer to build up the full network path + + StringBuffer strBuf = new StringBuffer(128); + + // Add the path, if there is one + + if (getPath().length() > 0) + { + if (getPath().charAt(0) != '\\') + { + strBuf.append("\\"); + } + strBuf.append(getPath()); + } + + // Add the file name if there is one + + if (getFileName().length() > 0) + { + if (strBuf.charAt(strBuf.length() - 1) != '\\') + { + strBuf.append("\\"); + } + strBuf.append(getFileName()); + } + + // Return the network path + + return strBuf.toString(); + } + + /** + * Get the remote share name string. + * + * @return Remote share name string. + */ + + public final String getShareName() + { + return m_shrname; + } + + /** + * Get the remote user name string. + * + * @return Remote user name string required to access the remote share. + */ + + public final String getUserName() + { + return m_username != null ? m_username : ""; + } + + /** + * Get the primary protocol to connect with + * + * @return int + */ + public final int getPrimaryProtocol() + { + return m_primaryProto; + } + + /** + * Get the secondary protocol to connect with + * + * @return int + */ + public final int getSecondaryProtocol() + { + return m_secondaryProto; + } + + /** + * Determine if the share has a domain specified. + * + * @return boolean + */ + public final boolean hasDomain() + { + return m_domain == null ? false : true; + } + + /** + * Set the domain to be used during the session setup. + * + * @param domain java.lang.String + */ + public final void setDomain(String domain) + { + m_domain = domain; + if (m_domain != null) + m_domain = m_domain.toUpperCase(); + } + + /** + * Set the remote file name string. + * + * @param fn Remote file name string. + */ + + public final void setFileName(String fn) + { + m_fname = fn; + } + + /** + * Set the PC share from the supplied UNC path string. + * + * @param netpath UNC format remote file path. + */ + + public final void setNetworkPath(String netpath) throws InvalidUNCPathException + { + + // Take a copy of the network path + + StringBuffer path = new StringBuffer(netpath); + for (int i = 0; i < path.length(); i++) + { + + // Convert forward slashes to back slashes + + if (path.charAt(i) == '/') + path.setCharAt(i, '\\'); + } + String npath = path.toString(); + + // UNC path starts with '\\' + + if (!npath.startsWith("\\\\") || npath.length() < 5) + throw new InvalidUNCPathException(npath); + + // Extract the node name from the network path + + int pos = 2; + int endpos = npath.indexOf("\\", pos); + + if (endpos == -1) + throw new InvalidUNCPathException(npath); + + setNodeName(npath.substring(pos, endpos)); + pos = endpos + 1; + + // Extract the share name from the network path + + endpos = npath.indexOf("\\", pos); + + if (endpos == -1) + { + + // Share name is the last part of the UNC path + + setShareName(npath.substring(pos)); + + // Set the root path and clear the file name + + setPath("\\"); + setFileName(""); + } + else + { + setShareName(npath.substring(pos, endpos)); + + pos = endpos + 1; + + // Extract the share relative path from the network path + + endpos = npath.lastIndexOf("\\"); + + if (endpos != -1 && endpos > pos) + { + + // Set the share relative path, and update the current position index + + setPath(npath.substring(pos, endpos)); + + // File name is the rest of the UNC path + + setFileName(npath.substring(endpos + 1)); + } + else + { + + // Set the share relative path to the root path + + setPath("\\"); + + // Set the file name string + + if (npath.length() > pos) + setFileName(npath.substring(pos)); + else + setFileName(""); + } + } + + // Check if the share name contains embedded access control + + pos = m_shrname.indexOf("%"); + if (pos != -1) + { + + // Find the end of the user name + + endpos = m_shrname.indexOf(":", pos); + if (endpos != -1) + { + + // Extract the user name and password strings + + setUserName(m_shrname.substring(pos + 1, endpos)); + setPassword(m_shrname.substring(endpos + 1)); + } + else + { + + // Extract the user name string + + setUserName(m_shrname.substring(pos + 1)); + } + + // Reset the share name string, to remove the access control + + setShareName(m_shrname.substring(0, pos)); + } + + // Check if the path has been set, if not then use the root path + + if (m_path == null || m_path.length() == 0) + m_path = "\\"; + } + + /** + * Set the extended security negotiation flags + * + * @param extFlags int + */ + public final void setExtendedSecurityFlags(int extFlags) + { + m_extendedSecFlags = extFlags; + } + + /** + * Set the remote node name string. + * + * @param nname Remote node name string. + */ + + public final void setNodeName(String nname) + { + m_nodename = nname; + } + + /** + * Set the remote password string. + * + * @param pwd Remote password string, required to access the remote share. + */ + + public final void setPassword(String pwd) + { + m_password = pwd; + } + + /** + * Set the share relative path string. + * + * @param pth Share relative path string. + */ + + public final void setPath(String pth) + { + m_path = pth; + } + + /** + * Set the remote share name string. + * + * @param shr Remote share name string. + */ + + public final void setShareName(String shr) + { + m_shrname = shr; + } + + /** + * Set the remote user name string. + * + * @param uname Remote user name string. + */ + + public final void setUserName(String uname) + { + m_username = uname; + } + + /** + * Set the primary and secondary protocol order that is used to connect to the remote host. + * + * @param pri int + * @param sec int + */ + public final void setProtocolOrder(int pri, int sec) + { + m_primaryProto = pri; + m_secondaryProto = sec; + } + + /** + * Return the PCShare object as a string + * + * @return PCShare string. + */ + + public final String toString() + { + return getNetworkPath(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/PacketType.java b/source/java/org/alfresco/filesys/smb/PacketType.java new file mode 100644 index 0000000000..a081f57dae --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/PacketType.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB packet type class + */ +public class PacketType +{ + // SMB packet types + + public static final int CreateDirectory = 0x00; + public static final int DeleteDirectory = 0x01; + public static final int OpenFile = 0x02; + public static final int CreateFile = 0x03; + public static final int CloseFile = 0x04; + public static final int FlushFile = 0x05; + public static final int DeleteFile = 0x06; + public static final int RenameFile = 0x07; + public static final int GetFileAttributes = 0x08; + public static final int SetFileAttributes = 0x09; + public static final int ReadFile = 0x0A; + public static final int WriteFile = 0x0B; + public static final int LockFile = 0x0C; + public static final int UnLockFile = 0x0D; + public static final int CreateTemporary = 0x0E; + public static final int CreateNew = 0x0F; + public static final int CheckDirectory = 0x10; + + public static final int ProcessExit = 0x11; + public static final int SeekFile = 0x12; + public static final int LockAndRead = 0x13; + public static final int WriteAndUnlock = 0x14; + public static final int ReadRaw = 0x1A; + public static final int ReadMpx = 0x1B; + public static final int ReadMpxSecondary = 0x1C; + public static final int WriteRaw = 0x1D; + public static final int WriteMpx = 0x1E; + public static final int WriteComplete = 0x20; + public static final int SetInformation2 = 0x22; + public static final int QueryInformation2 = 0x23; + public static final int LockingAndX = 0x24; + public static final int Transaction = 0x25; + public static final int TransactionSecond = 0x26; + public static final int IOCtl = 0x27; + public static final int IOCtlSecondary = 0x28; + public static final int Copy = 0x29; + public static final int Move = 0x2A; + public static final int Echo = 0x2B; + public static final int WriteAndClose = 0x2C; + public static final int OpenAndX = 0x2D; + public static final int ReadAndX = 0x2E; + public static final int WriteAndX = 0x2F; + public static final int CloseAndTreeDisc = 0x31; + public static final int Transaction2 = 0x32; + public static final int Transaction2Second= 0x33; + public static final int FindClose2 = 0x34; + public static final int FindNotifyClose = 0x35; + + public static final int TreeConnect = 0x70; + public static final int TreeDisconnect = 0x71; + public static final int Negotiate = 0x72; + public static final int SessionSetupAndX = 0x73; + public static final int LogoffAndX = 0x74; + public static final int TreeConnectAndX = 0x75; + + public static final int DiskInformation = 0x80; + public static final int Search = 0x81; + public static final int Find = 0x82; + public static final int FindUnique = 0x83; + + public static final int NTTransact = 0xA0; + public static final int NTTransactSecond = 0xA1; + public static final int NTCreateAndX = 0xA2; + public static final int NTCancel = 0xA4; + + public static final int OpenPrintFile = 0xC0; + public static final int WritePrintFile = 0xC1; + public static final int ClosePrintFile = 0xC2; + public static final int GetPrintQueue = 0xC3; + + // Send message codes + + public static final int SendMessage = 0xD0; + public static final int SendBroadcast = 0xD1; + public static final int SendForward = 0xD2; + public static final int CancelForward = 0xD3; + public static final int GetMachineName = 0xD4; + public static final int SendMultiStart = 0xD5; + public static final int SendMultiEnd = 0xD6; + public static final int SendMultiText = 0xD7; + + // Transaction2 operation codes + + public static final int Trans2Open = 0x00; + public static final int Trans2FindFirst = 0x01; + public static final int Trans2FindNext = 0x02; + public static final int Trans2QueryFileSys= 0x03; + public static final int Trans2QueryPath = 0x05; + public static final int Trans2SetPath = 0x06; + public static final int Trans2QueryFile = 0x07; + public static final int Trans2SetFile = 0x08; + public static final int Trans2CreateDir = 0x0D; + public static final int Trans2SessSetup = 0x0E; + + // Remote admin protocol (RAP) codes + + public static final int RAPShareEnum = 0; + public static final int RAPShareGetInfo = 1; + public static final int RAPSessionEnum = 6; + public static final int RAPServerGetInfo = 13; + public static final int NetServerDiskEnum = 15; + public static final int NetGroupEnum = 47; + public static final int RAPUserGetInfo = 56; + public static final int RAPWkstaGetInfo = 63; + public static final int RAPServerEnum = 94; + public static final int RAPServerEnum2 = 104; + public static final int RAPWkstaUserLogon = 132; + public static final int RAPWkstaUserLogoff= 133; + public static final int RAPChangePassword = 214; + + // Service information/control codes + + public static final int NetServiceEnum = 39; + public static final int NetServiceInstall = 40; + public static final int NetServiceControl = 41; + + // User/group information codes + + public static final int NetGroupGetUsers = 52; + public static final int NetUserEnum = 53; + public static final int NetUserGetGroups = 59; + + // Printer/print queue admin codes + + public static final int NetPrintQEnum = 69; + public static final int NetPrintQGetInfo = 70; + public static final int NetPrintQSetInfo = 71; + public static final int NetPrintQAdd = 72; + public static final int NetPrintQDel = 73; + public static final int NetPrintQPause = 74; + public static final int NetPrintQContinue = 75; + public static final int NetPrintJobEnum = 76; + public static final int NetPrintJobGetInfo= 77; + public static final int NetPrintJobSetInfo= 78; + public static final int NetPrintJobDelete = 81; + public static final int NetPrintJobPause = 82; + public static final int NetPrintJobContinue = 83; + public static final int NetPrintDestEnum = 84; + public static final int NetPrintDestGetInfo = 85; + public static final int NetPrintDestControl = 86; + + // Transaction named pipe sub-commands + + public static final int CallNamedPipe = 0x54; + public static final int WaitNamedPipe = 0x53; + public static final int PeekNmPipe = 0x23; + public static final int QNmPHandState = 0x21; + public static final int SetNmPHandState = 0x01; + public static final int QNmPipeInfo = 0x22; + public static final int TransactNmPipe = 0x26; + public static final int RawReadNmPipe = 0x11; + public static final int RawWriteNmPipe = 0x31; + + // Miscellaneous codes + + public static final int NetBIOSEnum = 92; + + // NT transaction function codes + + public static final int NTTransCreate = 1; + public static final int NTTransIOCtl = 2; + public static final int NTTransSetSecurityDesc = 3; + public static final int NTTransNotifyChange = 4; + public static final int NTTransRename = 5; + public static final int NTTransQuerySecurityDesc = 6; + public static final int NTTransGetUserQuota = 7; + public static final int NTTransSetUserQuota = 8; + + // Flag to indicate no chained AndX command + + public static final int NoChainedCommand = 0xFF; + + // SMB command names (block 1) + + private static String[] _cmdNames1 = { "CreateDirectory", + "DeleteDirectory", + "OpenFile", + "CreateFile", + "CloseFile", + "FlushFile", + "DeleteFile", + "RenameFile", + "GetFileAttributes", + "SetFileAttributes", + "ReadFile", + "WriteFile", + "LockFile", + "UnLockFile", + "CreateTemporary", + "CreateNew", + "CheckDirectory", + "ProcessExit", + "SeekFile", + "LockAndRead", + "WriteAndUnlock", + null, + null, + null, + null, + null, + "ReadRaw", + "ReadMpx", + "ReadMpxSecondary", + "WriteRaw", + "WriteMpx", + null, + "WriteComplete", + null, + "SetInformation2", + "QueryInformation2", + "LockingAndX", + "Transaction", + "TransactionSecond", + "IOCtl", + "IOCtlSecondary", + "Copy", + "Move", + "Echo", + "WriteAndClose", + "OpenAndX", + "ReadAndX", + "WriteAndX", + null, + "CloseAndTreeDisconnect", + "Transaction2", + "Transaction2Secondary", + "FindClose2", + "FindNotifyClose" + }; + + private static String[] _cmdNames2 = { "TreeConnect", + "TreeDisconnect", + "Negotiate", + "SessionSetupAndX", + "LogoffAndX", + "TreeConnectAndX" + }; + + private static String[] _cmdNames3 = { "DiskInformation", + "Search", + "Find", + "FindUnique" + }; + + private static String[] _cmdNames4 = { "NTTransact", + "NTTransactSecondary", + "NTCreateAndX", + null, + "NTCancel" + }; + + private static String[] _cmdNames5 = { "OpenPrintFile", + "WritePrintFile", + "ClosePrintFile", + "GetPrintQueue" + }; + + private static String[] _cmdNames6 = { "SendMessage", + "SendBroadcast", + "SendForward", + "CancelForward", + "GetMachineName", + "SendMultiStart", + "SendMultiEnd", + "SendMultiText" + }; + + // Transaction2 operation code names + + private static String[] _transNames = { "Trans2Open", + "Trans2FindFirst", + "Trans2FindNext", + "Trans2QueryFileSys", + "Trans2QueryPath", + "Trans2SetPath", + "Trans2QueryFile", + "Trans2SetFile", + "Trans2CreateDirectory", + "Trans2SessionSetup" + }; + + // NT transaction operation code names + + private static String[] _ntTranNames = { "", // zero not used + "NTTransCreate", + "NTTransIOCtl", + "NTTransSetSecurityDesc", + "NTTransNotifyChange", + "NTTransRename", + "NTTransQuerySecurityDesc", + "NTTransGetUserQuota", + "NTTransSetUserQuota" + }; + + /** + * Return an SMB command as a string + * + * @param cmd int + * @return String + */ + public static final String getCommandName(int cmd) + { + + // Get the command name + + String cmdName = ""; + + if (cmd >= 0 && cmd < _cmdNames1.length) + { + + // Get the command name from the main name table + + cmdName = _cmdNames1[cmd]; + } + else + { + + // Mask the command to determine the command table to index + + int cmdTop = cmd & 0x00F0; + + switch (cmd & 0x00F0) + { + case 0x70: + cmdName = _cmdNames2[cmd - 0x70]; + break; + case 0x80: + cmdName = _cmdNames3[cmd - 0x80]; + break; + case 0xA0: + cmdName = _cmdNames4[cmd - 0xA0]; + break; + case 0xC0: + cmdName = _cmdNames5[cmd - 0xC0]; + break; + case 0xD0: + cmdName = _cmdNames6[cmd - 0xD0]; + break; + default: + cmdName = "0x" + Integer.toHexString(cmd); + break; + } + } + + // Return the command name string + + return cmdName; + } + + /** + * Return a transaction code as a string + * + * @param opcode int + * @return String + */ + public final static String getTransactionName(int opcode) + { + + // Range check the opcode + + String opcodeName = ""; + + if (opcode >= 0 && opcode < _transNames.length) + opcodeName = _transNames[opcode]; + return opcodeName; + } + + /** + * Return an NT transation code as a string + * + * @param opcode int + * @return String + */ + public final static String getNTTransationName(int opcode) + { + + // Range check the opcode + + String opcodeName = ""; + + if (opcode >= 0 && opcode < _ntTranNames.length) + opcodeName = _ntTranNames[opcode]; + return opcodeName; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/Protocol.java b/source/java/org/alfresco/filesys/smb/Protocol.java new file mode 100644 index 0000000000..c0607d8c83 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/Protocol.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Protocol Class + *

    + * Declares constants for the available SMB protocols (TCP/IP NetBIOS and native TCP/IP SMB) + */ +public class Protocol +{ + + // Available protocol types + + public final static int TCPNetBIOS = 1; + public final static int NativeSMB = 2; + + // Protocol control constants + + public final static int UseDefault = 0; + public final static int None = -1; + + /** + * Return the protocol type as a string + * + * @param typ int + * @return String + */ + public static final String asString(int typ) + { + String ret = ""; + if (typ == TCPNetBIOS) + ret = "TCP/IP NetBIOS"; + else if (typ == NativeSMB) + ret = "Native SMB (port 445)"; + + return ret; + } +} diff --git a/source/java/org/alfresco/filesys/smb/SMBDate.java b/source/java/org/alfresco/filesys/smb/SMBDate.java new file mode 100644 index 0000000000..ed038a451d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SMBDate.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +import java.util.Calendar; +import java.util.Date; + +/** + * SMB date/time class. + */ +public final class SMBDate extends Date +{ + private static final long serialVersionUID = 3258407335553806902L; + + // Constants + // + // Bit masks for extracting the date/time fields from an SMB encoded date/time. + // + + private static final int Days = 0x001F; + private static final int Month = 0x01E0; + private static final int Year = 0xFE00; + + private static final int TwoSeconds = 0x001F; + private static final int Minutes = 0x07E0; + private static final int Hours = 0xF800; + + /** + * Construct the SMBDate using a seconds since 1-Jan-1970 00:00:00 value. + * + * @param secs Seconds since base date/time 1970 value + */ + + public SMBDate(int secs) + { + super((long) (secs & 0x7FFFFFFF)); + } + + /** + * Construct the SMBDate using the SMB encoded date/time values. + * + * @param dat SMB encoded date value + * @param tim SMB encoded time value + */ + + public SMBDate(int dat, int tim) + { + + // Extract the date from the SMB encoded value + + int days = dat & Days; + int months = (dat & Month) >> 5; + int year = (dat & Year) >> 9; + + // Extract the time from the SMB encoded value + + int secs = (tim & TwoSeconds) * 2; + int mins = (tim & Minutes) >> 5; + int hours = (tim & Hours) >> 11; + + // Use a calendar object to create the date/time value + + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.set(year + 1980, months - 1, days, hours, mins, secs); + + // Initialize this dates raw value + + this.setTime(cal.getTime().getTime()); + } + + /** + * Create a new SMBDate using the long time value. + * + * @param dattim long + */ + public SMBDate(long dattim) + { + super(dattim); + } + + /** + * Return this date as an SMB encoded date. + * + * @return SMB encoded date value. + */ + + public final int asSMBDate() + { + + // Use a calendar object to get the day, month and year values + + Calendar cal = Calendar.getInstance(); + cal.setTime(this); + + // Build the SMB encoded date value + + int smbDate = cal.get(Calendar.DAY_OF_MONTH); + smbDate += (cal.get(Calendar.MONTH) + 1) << 5; + smbDate += (cal.get(Calendar.YEAR) - 1980) << 9; + + // Return the SMB encoded date value + + return smbDate; + } + + /** + * Return this time as an SMB encoded time. + * + * @return SMB encoded time value. + */ + + public final int asSMBTime() + { + + // Use a calendar object to get the hour, minutes and seconds values + + Calendar cal = Calendar.getInstance(); + cal.setTime(this); + + // Build the SMB encoded time value + + int smbTime = cal.get(Calendar.SECOND) / 2; + smbTime += cal.get(Calendar.MINUTE) << 5; + smbTime += cal.get(Calendar.HOUR_OF_DAY) << 11; + + // Return the SMB encoded time value + + return smbTime; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/SMBDeviceType.java b/source/java/org/alfresco/filesys/smb/SMBDeviceType.java new file mode 100644 index 0000000000..ac4c13422d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SMBDeviceType.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB device types class. + *

    + * The class provides symbols for the remote device types that may be connected to. The values are + * also used when returning remote share information. + */ +public class SMBDeviceType +{ + + // Device type constants + + public static final int Disk = 0; + public static final int Printer = 1; + public static final int Comm = 2; + public static final int Pipe = 3; + public static final int Unknown = -1; + + /** + * Convert the device type to a string + * + * @param devtyp Device type + * @return Device type string + */ + public static String asString(int devtyp) + { + String devStr = null; + + switch (devtyp) + { + case Disk: + devStr = "Disk"; + break; + case Printer: + devStr = "Printer"; + break; + case Pipe: + devStr = "Pipe"; + break; + case Comm: + devStr = "Comm"; + break; + default: + devStr = "Unknown"; + break; + } + return devStr; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/SMBErrorText.java b/source/java/org/alfresco/filesys/smb/SMBErrorText.java new file mode 100644 index 0000000000..cc6c882135 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SMBErrorText.java @@ -0,0 +1,839 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB error text class. + *

    + * The SMBErrorText is a static class that converts SMB error class/error codes into their + * appropriate error message strings. The class is used by the SMBException class when outputting an + * SMB exception as a string. + *

    + * SMB error classes and error codes are declared in the SMBStatus class. + */ + +public final class SMBErrorText +{ + + /** + * Return the error string associated with the SMB error class/code + * + * @param errclass Error class. + * @param errcode Error code. + * @return Error string. + */ + public final static String ErrorString(int errclass, int errcode) + { + + // Determine the error class + + String errtext = null; + + switch (errclass) + { + + // Success class + + case SMBStatus.Success: + errtext = "The request was successful"; + break; + + // DOS error class + + case SMBStatus.ErrDos: + errtext = DOSErrorText(errcode); + break; + + // Server error class + + case SMBStatus.ErrSrv: + errtext = ServerErrorText(errcode); + break; + + // Hardware error class + + case SMBStatus.ErrHrd: + errtext = HardwareErrorText(errcode); + break; + + // Network error codes, returned by transaction requests + + case SMBStatus.NetErr: + errtext = NetworkErrorText(errcode); + break; + + // JLAN error codes + + case SMBStatus.JLANErr: + errtext = JLANErrorText(errcode); + break; + + // NT 32-bit error codes + + case SMBStatus.NTErr: + errtext = NTErrorText(errcode); + break; + + // Win32 error codes + + case SMBStatus.Win32Err: + errtext = Win32ErrorText(errcode); + break; + + // DCE/RPC error + + case SMBStatus.DCERPCErr: + errtext = DCERPCErrorText(errcode); + break; + + // Bad SMB command + + case SMBStatus.ErrCmd: + errtext = "Command was not in the SMB format"; + break; + } + + if (errtext == null) + errtext = "[Unknown error status/class: " + errclass + "," + errcode + "]"; + + // Return the error text + + return errtext; + } + + /** + * Return a DOS error string. + * + * @param errcode DOS error code. + * @return DOS error string. + */ + + private static String DOSErrorText(int errcode) + { + + // Convert the DOS error code to a text string + + String errtext = null; + + switch (errcode) + { + case 1: + errtext = "Invalid function. Server did not recognize/perform system call"; + break; + case 2: + errtext = "File not found"; + break; + case 3: + errtext = "Directory invalid"; + break; + case 4: + errtext = "Too many open files"; + break; + case 5: + errtext = "Access denied"; + break; + case 6: + errtext = "Invalid file handle"; + break; + case 7: + errtext = "Memory control blocks destroyed"; + break; + case 8: + errtext = "Insufficient server memory to perform function"; + break; + case 9: + errtext = "Invalid memory block address"; + break; + case 10: + errtext = "Invalid environment"; + break; + case 11: + errtext = "Invalid format"; + break; + case 12: + errtext = "Invalid open mode"; + break; + case 13: + errtext = "Invalid data, in server IOCTL call"; + break; + case 15: + errtext = "Invalid drive specified"; + break; + case 16: + errtext = "Delete directory attempted to delete servers directory"; + break; + case 17: + errtext = "Not same device"; + break; + case 18: + errtext = "No more files"; + break; + case 32: + errtext = "File sharing mode conflict"; + break; + case 33: + errtext = "Lock request conflicts with existing lock"; + break; + case 66: + errtext = "IPC not supported"; + break; + case 80: + errtext = "File already exists"; + break; + case 110: + errtext = "Cannot open the file specified"; + break; + case 124: + errtext = "Unknown information level"; + break; + case SMBStatus.DOSDirectoryNotEmpty: + errtext = "Directory not empty"; + break; + case 230: + errtext = "Named pipe invalid"; + break; + case 231: + errtext = "All instances of pipe are busy"; + break; + case 232: + errtext = "Named pipe close in progress"; + break; + case 233: + errtext = "No process on other end of named pipe"; + break; + case 234: + errtext = "More data to be returned"; + break; + case 267: + errtext = "Invalid directory name in path"; + break; + case 275: + errtext = "Extended attributes did not fit"; + break; + case 282: + errtext = "Extended attributes not supported"; + break; + case 2142: + errtext = "Unknown IPC"; + break; + } + + // Return the error string + + return errtext; + } + + /** + * Return a hardware error string. + * + * @param errcode Hardware error code. + * @return Hardware error string. + */ + private final static String HardwareErrorText(int errcode) + { + + // Convert the hardware error code to a text string + + String errtext = null; + + switch (errcode) + { + case 19: + errtext = "Attempt to write on write protected media"; + break; + case 20: + errtext = "Unknown unit"; + break; + case 21: + errtext = "Drive not ready"; + break; + case 22: + errtext = "Unknown command"; + break; + case 23: + errtext = "Data error (CRC)"; + break; + case 24: + errtext = "Bad request structure length"; + break; + case 25: + errtext = "Seek error"; + break; + case 26: + errtext = "Unknown media type"; + break; + case 27: + errtext = "Sector not found"; + break; + case 28: + errtext = "Printer out of paper"; + break; + case 29: + errtext = "Write fault"; + break; + case 30: + errtext = "Read fault"; + break; + case 31: + errtext = "General failure"; + break; + case 32: + errtext = "Open conflicts with existing open"; + break; + case 33: + errtext = "Lock request conflicted with existing lock"; + break; + case 34: + errtext = "Wrong disk was found in a drive"; + break; + case 35: + errtext = "No FCBs are available to process request"; + break; + case 36: + errtext = "A sharing buffer has been exceeded"; + break; + } + + // Return the error string + + return errtext; + } + + /** + * Return a JLAN error string. + * + * @return java.lang.String + * @param errcode int + */ + private static String JLANErrorText(int errcode) + { + + // Convert the JLAN error code to a text string + + String errtext = null; + + switch (errcode) + { + case SMBStatus.JLANUnsupportedDevice: + errtext = "Invalid device type for dialect"; + break; + case SMBStatus.JLANNoMoreSessions: + errtext = "No more sessions available"; + break; + case SMBStatus.JLANSessionNotActive: + errtext = "Session is not active"; + break; + case SMBStatus.JLANInvalidSMBReceived: + errtext = "Invalid SMB response received"; + break; + case SMBStatus.JLANLargeFilesNotSupported: + errtext = "Large files not supported"; + break; + case SMBStatus.JLANInvalidFileInfo: + errtext = "Invalid file information for level"; + break; + case SMBStatus.JLANDceRpcNotSupported: + errtext = "Server does not support DCE/RPC requests"; + break; + } + return errtext; + } + + /** + * Return a network error string. + * + * @param errcode Network error code. + * @return Network error string. + */ + private final static String NetworkErrorText(int errcode) + { + + // Convert the network error code to a text string + + String errtext = null; + + switch (errcode) + { + case SMBStatus.NETAccessDenied: + errtext = "Access denied"; + break; + case SMBStatus.NETInvalidHandle: + errtext = "Invalid handle"; + break; + case SMBStatus.NETUnsupported: + errtext = "Function not supported"; + break; + case SMBStatus.NETBadDeviceType: + errtext = "Bad device type"; + break; + case SMBStatus.NETBadNetworkName: + errtext = "Bad network name"; + break; + case SMBStatus.NETAlreadyAssigned: + errtext = "Already assigned"; + break; + case SMBStatus.NETInvalidPassword: + errtext = "Invalid password"; + break; + case SMBStatus.NETInvParameter: + errtext = "Incorrect parameter"; + break; + case SMBStatus.NETContinued: + errtext = "Transaction continued ..."; + break; + case SMBStatus.NETNoMoreItems: + errtext = "No more items"; + break; + case SMBStatus.NETInvalidAddress: + errtext = "Invalid address"; + break; + case SMBStatus.NETServiceDoesNotExist: + errtext = "Service does not exist"; + break; + case SMBStatus.NETBadDevice: + errtext = "Bad device"; + break; + case SMBStatus.NETNoNetOrBadPath: + errtext = "No network or bad path"; + break; + case SMBStatus.NETExtendedError: + errtext = "Extended error"; + break; + case SMBStatus.NETNoNetwork: + errtext = "No network"; + break; + case SMBStatus.NETCancelled: + errtext = "Cancelled"; + break; + case SMBStatus.NETSrvNotRunning: + errtext = "Server service is not running"; + break; + case SMBStatus.NETBufferTooSmall: + errtext = "Supplied buffer is too small"; + break; + case SMBStatus.NETNoTransactions: + errtext = "Server is not configured for transactions"; + break; + case SMBStatus.NETInvQueueName: + errtext = "Invalid queue name"; + break; + case SMBStatus.NETNoSuchPrintJob: + errtext = "Specified print job could not be located"; + break; + case SMBStatus.NETNotResponding: + errtext = "Print process is not responding"; + break; + case SMBStatus.NETSpoolerNotStarted: + errtext = "Spooler is not started on the remote server"; + break; + case SMBStatus.NETCannotPerformOp: + errtext = "Operation cannot be performed on the print job in it's current state"; + break; + case SMBStatus.NETErrLoadLogonScript: + errtext = "Error occurred running/loading logon script"; + break; + case SMBStatus.NETLogonNotValidated: + errtext = "Logon was not validated by any server"; + break; + case SMBStatus.NETLogonSrvOldSoftware: + errtext = "Logon server is running old software version, cannot validate logon"; + break; + case SMBStatus.NETUserNameNotFound: + errtext = "User name was not found"; + break; + case SMBStatus.NETUserLgnWkNotAllowed: + errtext = "User is not allowed to logon from this computer"; + break; + case SMBStatus.NETUserLgnTimeNotAllowed: + errtext = "USer is not allowed to logon at this time"; + break; + case SMBStatus.NETUserPasswordExpired: + errtext = "User password has expired"; + break; + case SMBStatus.NETPasswordCannotChange: + errtext = "Password cannot be changed"; + break; + case SMBStatus.NETPasswordTooShort: + errtext = "Password is too short"; + break; + } + + // Return the error string + + return errtext; + } + + /** + * Return a server error string. + * + * @param errcode Server error code. + * @return Server error string. + */ + private final static String ServerErrorText(int errcode) + { + + // Convert the server error code to a text string + + String errtext = null; + switch (errcode) + { + case 1: + errtext = "Non-specific error"; + break; + case 2: + errtext = "Bad password"; + break; + case 4: + errtext = "Client does not have access rights"; + break; + case 5: + errtext = "Invalid TID"; + break; + case 6: + errtext = "Invalid network name"; + break; + case 7: + errtext = "Invalid device"; + break; + case 49: + errtext = "Print queue full (files)"; + break; + case 50: + errtext = "Print queue full (space)"; + break; + case 51: + errtext = "EOF on print queue dump"; + break; + case 52: + errtext = "Invalid print file FID"; + break; + case 64: + errtext = "Server did not recognize the command received"; + break; + case 65: + errtext = "Internal server error"; + break; + case 67: + errtext = "FID and pathname combination invalid"; + break; + case 69: + errtext = "Invalid access permission"; + break; + case 71: + errtext = "Invalid attribute mode"; + break; + case 81: + errtext = "Server is paused"; + break; + case 82: + errtext = "Not receiving messages"; + break; + case 83: + errtext = "No room to buffer message"; + break; + case 87: + errtext = "Too many remote user names"; + break; + case 88: + errtext = "Operation timed out"; + break; + case 89: + errtext = "No resources available for request"; + break; + case 90: + errtext = "Too many UIDs active on session"; + break; + case 91: + errtext = "Invalid UID"; + break; + case 250: + errtext = "Unable to support RAW, use MPX"; + break; + case 251: + errtext = "Unable to support RAW, use standard read/write"; + break; + case 252: + errtext = "Continue in MPX mode"; + break; + case 65535: + errtext = "Function not supported"; + break; + } + + // Return the error string + + return errtext; + } + + /** + * Return an NT error string. + * + * @param errcode NT error code. + * @return NT error string. + */ + private final static String NTErrorText(int errcode) + { + + // Convert the NT error code to a text string + + String errtext = ""; + + switch (errcode) + { + case SMBStatus.NTSuccess: + errtext = "The request was successful"; + break; + case SMBStatus.NTAccessDenied: + errtext = "Access denied"; + break; + case SMBStatus.NTObjectNotFound: + errtext = "Object not found"; + break; + case SMBStatus.Win32InvalidHandle: + errtext = "Invalid handle"; + break; + case SMBStatus.Win32BadDeviceType: + errtext = "Bad device type"; + break; + case SMBStatus.Win32BadNetworkName: + errtext = "Bad network name"; + break; + case SMBStatus.Win32AlreadyAssigned: + errtext = "Already assigned"; + break; + case SMBStatus.Win32InvalidPassword: + errtext = "Invalid password"; + break; + case SMBStatus.NTInvalidParameter: + errtext = "Invalid parameter"; + break; + case SMBStatus.Win32MoreData: + errtext = "More data available"; + break; + case SMBStatus.Win32NoMoreItems: + errtext = "No more items"; + break; + case SMBStatus.Win32InvalidAddress: + errtext = "Invalid address"; + break; + case SMBStatus.Win32ServiceDoesNotExist: + errtext = "Service does not exist"; + break; + case SMBStatus.Win32BadDevice: + errtext = "Bad device"; + break; + case SMBStatus.Win32NoNetOrBadPath: + errtext = "No network or bad path"; + break; + case SMBStatus.Win32ExtendedError: + errtext = "Extended error"; + break; + case SMBStatus.Win32NoNetwork: + errtext = "No network"; + break; + case SMBStatus.NTCancelled: + errtext = "Cancelled"; + break; + case SMBStatus.NTBufferOverflow: + errtext = "Buffer overflow"; + break; + case SMBStatus.NTNoSuchFile: + errtext = "No such file"; + break; + case SMBStatus.NTInvalidDeviceRequest: + errtext = "Invalid device request"; + break; + case SMBStatus.NTMoreProcessingRequired: + errtext = "More processing required"; + break; + case SMBStatus.NTInvalidSecDescriptor: + errtext = "Invalid security descriptor"; + break; + case SMBStatus.NTNotSupported: + errtext = "Not supported"; + break; + case SMBStatus.NTBadDeviceType: + errtext = "Bad device type"; + break; + case SMBStatus.NTObjectPathNotFound: + errtext = "Object path not found"; + break; + case SMBStatus.NTLogonFailure: + errtext = "Logon failure"; + break; + case SMBStatus.NTAccountDisabled: + errtext = "Account disabled"; + break; + case SMBStatus.NTNoneMapped: + errtext = "None mapped"; + break; + case SMBStatus.NTInvalidInfoClass: + errtext = "Invalid information class"; + break; + case SMBStatus.NTObjectNameCollision: + errtext = "Object name collision"; + break; + case SMBStatus.NTNotImplemented: + errtext = "Not implemented"; + break; + case SMBStatus.NTFileOffline: + errtext = "File is offline"; + break; + case SMBStatus.NTSharingViolation: + errtext = "Sharing violation"; + break; + case SMBStatus.NTBadNetName: + errtext = "Bad network name"; + break; + case SMBStatus.NTBufferTooSmall: + errtext = "Buffer too small"; + break; + case SMBStatus.NTLockConflict: + errtext = "Lock conflict"; + break; + case SMBStatus.NTLockNotGranted: + errtext = "Lock not granted"; + break; + case SMBStatus.NTRangeNotLocked: + errtext = "Range not locked"; + break; + case SMBStatus.NTDiskFull: + errtext = "Disk full"; + break; + case SMBStatus.NTTooManyOpenFiles: + errtext = "Too many open files"; + break; + case SMBStatus.NTRequestNotAccepted: + errtext = "Request not accepted"; + break; + case SMBStatus.NTNoSuchDomain: + errtext = "No such domain"; + break; + case SMBStatus.NTNoMoreFiles: + errtext = "No more files"; + break; + case SMBStatus.NTObjectNameInvalid: + errtext = "Object name invalid"; + break; + case SMBStatus.NTPipeBusy: + errtext = "Pipe is busy"; + break; + default: + errtext = "Unknown NT status 0x" + Integer.toHexString(errcode); + break; + } + return errtext; + } + + /** + * Return a Win32 error string. + * + * @param errcode Win32 error code. + * @return Win32 error string. + */ + private final static String Win32ErrorText(int errcode) + { + + // Convert the Win32 error code to a text string + + String errtext = ""; + + switch (errcode) + { + case SMBStatus.Win32FileNotFound: + errtext = "File not found"; + break; + case SMBStatus.Win32PathNotFound: + errtext = "Path not found"; + break; + case SMBStatus.Win32AccessDenied: + errtext = "Access denied"; + break; + case SMBStatus.Win32InvalidHandle: + errtext = "Invalid handle"; + break; + case SMBStatus.Win32BadDeviceType: + errtext = "Bad device type"; + break; + case SMBStatus.Win32BadNetworkName: + errtext = "Bad network name"; + break; + case SMBStatus.Win32AlreadyAssigned: + errtext = "Already assigned"; + break; + case SMBStatus.Win32InvalidPassword: + errtext = "Invalid password"; + break; + case SMBStatus.Win32MoreEntries: + errtext = "More entries"; + break; + case SMBStatus.Win32MoreData: + errtext = "More data"; + break; + case SMBStatus.Win32NoMoreItems: + errtext = "No more items"; + break; + case SMBStatus.Win32InvalidAddress: + errtext = "Invalid address"; + break; + case SMBStatus.Win32ServiceDoesNotExist: + errtext = "Service does not exist"; + break; + case SMBStatus.Win32ServiceMarkedForDelete: + errtext = "Service marked for delete"; + break; + case SMBStatus.Win32ServiceExists: + errtext = "Service already exists"; + break; + case SMBStatus.Win32ServiceDuplicateName: + errtext = "Duplicate service name"; + break; + case SMBStatus.Win32BadDevice: + errtext = "Bad device"; + break; + case SMBStatus.Win32NoNetOrBadPath: + errtext = "No network or bad path"; + break; + case SMBStatus.Win32ExtendedError: + errtext = "Extended error"; + break; + case SMBStatus.Win32NoNetwork: + errtext = "No network"; + break; + default: + errtext = "Unknown Win32 status 0x" + Integer.toHexString(errcode); + break; + } + return errtext; + } + + /** + * Return a DCE/RPC error string. + * + * @param errcode DCE/RPC error code + * @return DCE/RPC error string. + */ + private final static String DCERPCErrorText(int errcode) + { + + // Convert the DCE/RPC error code to a text string + + if (errcode == SMBStatus.DCERPC_Fault) + return "DCE/RPC Fault"; + return "DCE/RPC Error 0x" + Integer.toHexString(errcode); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/SMBException.java b/source/java/org/alfresco/filesys/smb/SMBException.java new file mode 100644 index 0000000000..2ec50c192d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SMBException.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB exception class + *

    + * This class holds the detail of an SMB network error. The SMB error class and error code are + * available to give extra detail about the error condition. + */ +public class SMBException extends Exception +{ + private static final long serialVersionUID = 3256719593644176946L; + + // SMB error class + + protected int m_errorclass; + + // SMB error code + + protected int m_errorcode; + + /** + * Construct an SMB exception with the specified error class/error code. + */ + + public SMBException(int errclass, int errcode) + { + super(SMBErrorText.ErrorString(errclass, errcode)); + m_errorclass = errclass; + m_errorcode = errcode; + } + + /** + * Construct an SMB exception with the specified error class/error code and additional text + * error message. + */ + + public SMBException(int errclass, int errcode, String msg) + { + super(msg); + m_errorclass = errclass; + m_errorcode = errcode; + } + + /** + * Return the error class for this SMB exception. + * + * @return SMB error class. + */ + + public int getErrorClass() + { + return m_errorclass; + } + + /** + * Return the error code for this SMB exception + * + * @return SMB error code + */ + + public int getErrorCode() + { + return m_errorcode; + } + + /** + * Return the error text for the SMB exception + * + * @return Error text string. + */ + + public String getErrorText() + { + return SMBErrorText.ErrorString(m_errorclass, m_errorcode); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/SMBStatus.java b/source/java/org/alfresco/filesys/smb/SMBStatus.java new file mode 100644 index 0000000000..ffaba0c907 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SMBStatus.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * SMB status code class. + *

    + * The SMBStatus class contains the error class and error code values that a remote server may + * return. + *

    + * The available error classes are defined below :- + *

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    SMBStatus.SuccesIndicates that an SMB request was successful
    SMBStatus.ErrDOSError is from the DOS operating system set
    SMBStatus.ErrSrvError is from the server network file manager
    SMBStatus.ErrHrdError is a hardware type error
    SMBStatus.ErrCmdCommand was not in the SMB format
    SMBStatus.NetErrErrors returned by SMB transactions
    SMBStatus.NTErr32 bit errors returned when NT dialect is in use
    SMBStatus.DCERPCErrErrors returned by DCE/RPC requests
    SMBStatus.JLANErrJLAN error codes
    + */ +public final class SMBStatus +{ + + // Error classes + + public static final int Success = 0x00; + public static final int ErrDos = 0x01; + public static final int ErrSrv = 0x02; + public static final int ErrHrd = 0x03; + public static final int NetErr = 0x04; + public static final int JLANErr = 0x05; + public static final int NTErr = 0x06; + public static final int DCERPCErr = 0x07; + public static final int Win32Err = 0x08; + + public static final int ErrCmd = 0xFF; + + // Mask for NT severity + + public static final int NT_SEVERITY_MASK = 0xF0000000; + public static final int NT_ERROR_MASK = 0x0FFFFFFF; + + // DOS error codes. + + public static final int DOSInvalidFunc = 1; + public static final int DOSFileNotFound = 2; + public static final int DOSDirectoryInvalid = 3; + public static final int DOSTooManyOpenFiles = 4; + public static final int DOSAccessDenied = 5; + public static final int DOSInvalidHandle = 6; + public static final int DOSMemCtrlBlkDestoyed = 7; + public static final int DOSInsufficientMem = 8; + public static final int DOSInvalidAddress = 9; + public static final int DOSInvalidEnv = 10; + public static final int DOSInvalidFormat = 11; + public static final int DOSInvalidOpenMode = 12; + public static final int DOSInvalidData = 13; + public static final int DOSInvalidDrive = 15; + public static final int DOSDeleteSrvDir = 16; + public static final int DOSNotSameDevice = 17; + public static final int DOSNoMoreFiles = 18; + public static final int DOSFileSharingConflict = 32; + public static final int DOSLockConflict = 33; + public static final int DOSFileAlreadyExists = 80; + public static final int DOSUnknownInfoLevel = 124; + public static final int DOSDirectoryNotEmpty = 145; + public static final int DOSNotLocked = 158; + + // Server error codes + + public static final int SRVNonSpecificError = 1; + public static final int SRVBadPassword = 2; + public static final int SRVNoAccessRights = 4; + public static final int SRVInvalidTID = 5; + public static final int SRVInvalidNetworkName = 6; + public static final int SRVInvalidDevice = 7; + public static final int SRVPrintQueueFullFiles = 49; + public static final int SRVPrintQueueFullSpace = 50; + public static final int SRVEOFOnPrintQueueDump = 51; + public static final int SRVInvalidPrintFID = 52; + public static final int SRVUnrecognizedCommand = 64; + public static final int SRVInternalServerError = 65; + public static final int SRVFIDAndPathInvalid = 67; + public static final int SRVInvalidAccessPerm = 69; + public static final int SRVInvalidAttributeMode = 70; + public static final int SRVServerPaused = 81; + public static final int SRVNotReceivingMessages = 82; + public static final int SRVNoBuffers = 83; + public static final int SRVTooManyRemoteNames = 87; + public static final int SRVTimedOut = 88; + public static final int SRVNoResourcesAvailable = 89; + public static final int SRVTooManyUIDs = 90; + public static final int SRVInvalidUID = 91; + public static final int SRVNoRAWUseMPX = 250; + public static final int SRVNoRAWUseStdReadWrite = 251; + public static final int SRVContinueInMPXMode = 252; + public static final int SRVNotSupported = 65535; + + // Hardware error codes. + + public static final int HRDWriteProtected = 19; + public static final int HRDUnknownUnit = 20; + public static final int HRDDriveNotReady = 21; + public static final int HRDUnknownCommand = 22; + public static final int HRDDataError = 23; + public static final int HRDBadRequestLength = 24; + public static final int HRDSeekError = 25; + public static final int HRDUnknownMediaType = 26; + public static final int HRDSectorNotFound = 27; + public static final int HRDPrinterOutOfPaper = 28; + public static final int HRDWriteFault = 29; + public static final int HRDReadFault = 30; + public static final int HRDGeneralFailure = 31; + public static final int HRDOpenConflict = 32; + public static final int HRDLockConflict = 33; + public static final int HRDWrongDiskInDrive = 34; + public static final int HRDNoFCBsAvailable = 35; + public static final int HRDSharingBufferOverrun = 36; + + // Network error codes + + public static final int NETAccessDenied = 5; + public static final int NETInvalidHandle = 6; + public static final int NETUnsupported = 50; + public static final int NETNetAccessDenied = 65; + public static final int NETBadDeviceType = 66; + public static final int NETBadNetworkName = 67; + public static final int NETAlreadyAssigned = 85; + public static final int NETInvalidPassword = 86; + public static final int NETInvParameter = 87; + public static final int NETContinued = 234; + public static final int NETNoMoreItems = 259; + public static final int NETInvalidAddress = 487; + public static final int NETServiceDoesNotExist = 1060; + public static final int NETBadDevice = 1200; + public static final int NETNoNetOrBadPath = 1203; + public static final int NETExtendedError = 1208; + public static final int NETNoNetwork = 1222; + public static final int NETCancelled = 1223; + public static final int NETSrvNotRunning = 2114; + public static final int NETBufferTooSmall = 2123; + public static final int NETNoTransactions = 2141; + public static final int NETInvQueueName = 2150; + public static final int NETNoSuchPrintJob = 2151; + public static final int NETNotResponding = 2160; + public static final int NETSpoolerNotStarted = 2161; + public static final int NETCannotPerformOp = 2164; + public static final int NETErrLoadLogonScript = 2212; + public static final int NETLogonNotValidated = 2214; + public static final int NETLogonSrvOldSoftware = 2217; + public static final int NETUserNameNotFound = 2221; + public static final int NETUserLgnWkNotAllowed = 2240; + public static final int NETUserLgnTimeNotAllowed = 2241; + public static final int NETUserPasswordExpired = 2242; + public static final int NETPasswordCannotChange = 2243; + public static final int NETPasswordTooShort = 2246; + + // JLAN error codes + + public static final int JLANUnsupportedDevice = 1; + public static final int JLANNoMoreSessions = 2; + public static final int JLANSessionNotActive = 3; + public static final int JLANInvalidSMBReceived = 4; + public static final int JLANLargeFilesNotSupported = 5; + public static final int JLANInvalidFileInfo = 6; + public static final int JLANDceRpcNotSupported = 7; + + // NT 32-bit status code + + public static final int NTSuccess = 0; + + public static final int NTNotImplemented = 0xC0000002; + public static final int NTInvalidInfoClass = 0xC0000003; + public static final int NTInvalidParameter = 0xC000000D; + public static final int NTNoSuchFile = 0xC000000F; + public static final int NTInvalidDeviceRequest = 0xC0000010; + public static final int NTMoreProcessingRequired = 0xC0000016; + public static final int NTAccessDenied = 0xC0000022; + public static final int NTBufferTooSmall = 0xC0000023; + public static final int NTObjectNameInvalid = 0xC0000033; + public static final int NTObjectNotFound = 0xC0000034; + public static final int NTObjectNameCollision = 0xC0000035; + public static final int NTObjectPathNotFound = 0xC000003A; + public static final int NTSharingViolation = 0xC0000043; + public static final int NTLockConflict = 0xC0000054; + public static final int NTLockNotGranted = 0xC0000055; + public static final int NTLogonFailure = 0xC000006D; + public static final int NTAccountDisabled = 0xC0000072; + public static final int NTNoneMapped = 0xC0000073; + public static final int NTInvalidSecDescriptor = 0xC0000079; + public static final int NTRangeNotLocked = 0xC000007E; + public static final int NTDiskFull = 0xC000007F; + public static final int NTPipeBusy = 0xC00000AE; + public static final int NTNotSupported = 0xC00000BB; + public static final int NTBadDeviceType = 0xC00000CB; + public static final int NTBadNetName = 0xC00000CC; + public static final int NTRequestNotAccepted = 0xC00000D0; + public static final int NTNoSuchDomain = 0xC00000DF; + public static final int NTTooManyOpenFiles = 0xC000011F; + public static final int NTCancelled = 0xC0000120; + public static final int NTFileOffline = 0xC0000267; + + public static final int Win32FileNotFound = 2; + public static final int Win32PathNotFound = 3; + public static final int Win32AccessDenied = 5; + public static final int Win32InvalidHandle = 6; + public static final int Win32BadDeviceType = 66; + public static final int Win32BadNetworkName = 67; + public static final int Win32AlreadyAssigned = 85; + public static final int Win32InvalidPassword = 86; + public static final int Win32MoreData = 234; + public static final int Win32NoMoreItems = 259; + public static final int Win32MoreEntries = 261; + public static final int Win32InvalidAddress = 487; + public static final int Win32ServiceDoesNotExist = 1060; + public static final int Win32ServiceMarkedForDelete = 1072; + public static final int Win32ServiceExists = 1073; + public static final int Win32ServiceDuplicateName = 1077; + public static final int Win32BadDevice = 1200; + public static final int Win32NoNetOrBadPath = 1203; + public static final int Win32ExtendedError = 1208; + public static final int Win32NoNetwork = 1222; + + public static final int NTBufferOverflow = 0x80000005; + public static final int NTNoMoreFiles = 0x80000006; + public static final int NTNotifyEnumDir = 0x0000010C; + + // DEC/RPC status codes + + public static final int DCERPC_Fault = 0; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/SeekType.java b/source/java/org/alfresco/filesys/smb/SeekType.java new file mode 100644 index 0000000000..0ef36f33da --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SeekType.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Seek file position types. + */ +public class SeekType +{ + // Seek file types + + public static final int StartOfFile = 0; + public static final int CurrentPos = 1; + public static final int EndOfFile = 2; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/ServerType.java b/source/java/org/alfresco/filesys/smb/ServerType.java new file mode 100644 index 0000000000..8ca5a8fae5 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/ServerType.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +import org.alfresco.filesys.util.*; + +/** + * Server Type Flags Class + */ +public class ServerType +{ + + // Server type flags + + public static final int WorkStation = 0x00000001; + public static final int Server = 0x00000002; + public static final int SQLServer = 0x00000004; + public static final int DomainCtrl = 0x00000008; + public static final int DomainBakCtrl = 0x00000010; + public static final int TimeSource = 0x00000020; + public static final int AFPServer = 0x00000040; + public static final int NovellServer = 0x00000080; + public static final int DomainMember = 0x00000100; + public static final int PrintServer = 0x00000200; + public static final int DialinServer = 0x00000400; + public static final int UnixServer = 0x00000800; + public static final int NTServer = 0x00001000; + public static final int WfwServer = 0x00002000; + public static final int MFPNServer = 0x00004000; + public static final int NTNonDCServer = 0x00008000; + public static final int PotentialBrowse = 0x00010000; + public static final int BackupBrowser = 0x00020000; + public static final int MasterBrowser = 0x00040000; + public static final int DomainMaster = 0x00080000; + public static final int OSFServer = 0x00100000; + public static final int VMSServer = 0x00200000; + public static final int Win95Plus = 0x00400000; + public static final int DFSRoot = 0x00800000; + public static final int NTCluster = 0x01000000; + public static final int TerminalServer = 0x02000000; + public static final int DCEServer = 0x10000000; + public static final int AlternateXport = 0x20000000; + public static final int LocalListOnly = 0x40000000; + + public static final int DomainEnum = 0x80000000; + + // Server type strings + + private static final String[] _srvType = { + "Workstation", + "Server", + "SQLServer", + "DomainController", + "BackupDomainController", + "TimeSource", + "AFPServer", + "NovellServer", + "DomainMember", + "PrintServer", + "DialinServer", + "UnixServer", + "NTServer", + "WfwServer", + "MFPNServer", + "NtNonDCServer", + "PotentialBrowse", + "BackupBrowser", + "MasterBrowser", + "DomainMaster", + "OSFServer", + "VMSServer", + "Win95Plus", + "DFSRoot", + "NTCluster", + "TerminalServer", + "", + "", + "DCEServer" }; + + /** + * Convert server type flags to a list of server type strings + * + * @param typ int + * @return StringList + */ + public static final StringList TypeAsStrings(int typ) + { + // Allocate the vector for the strings + + StringList strs = new StringList(); + + // Test each type bit and add the appropriate type string + + for (int i = 0; i < _srvType.length; i++) + { + // Check the current type flag + + int mask = 1 << i; + if ((typ & mask) != 0) + strs.addString(_srvType[i]); + } + + // Return the list of type strings + + return strs; + } + + /** + * Check if the workstation flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isWorkStation(int typ) + { + return (typ & WorkStation) != 0 ? true : false; + } + + /** + * Check if the server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isServer(int typ) + { + return (typ & Server) != 0 ? true : false; + } + + /** + * Check if the SQL server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isSQLServer(int typ) + { + return (typ & SQLServer) != 0 ? true : false; + } + + /** + * Check if the domain controller flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isDomainController(int typ) + { + return (typ & DomainCtrl) != 0 ? true : false; + } + + /** + * Check if the backup domain controller flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isBackupDomainController(int typ) + { + return (typ & DomainBakCtrl) != 0 ? true : false; + } + + /** + * Check if the time source flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isTimeSource(int typ) + { + return (typ & TimeSource) != 0 ? true : false; + } + + /** + * Check if the AFP server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isAFPServer(int typ) + { + return (typ & AFPServer) != 0 ? true : false; + } + + /** + * Check if the Novell server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isNovellServer(int typ) + { + return (typ & NovellServer) != 0 ? true : false; + } + + /** + * Check if the domain member flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isDomainMember(int typ) + { + return (typ & DomainMember) != 0 ? true : false; + } + + /** + * Check if the print server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isPrintServer(int typ) + { + return (typ & PrintServer) != 0 ? true : false; + } + + /** + * Check if the dialin server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isDialinServer(int typ) + { + return (typ & DialinServer) != 0 ? true : false; + } + + /** + * Check if the Unix server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isUnixServer(int typ) + { + return (typ & UnixServer) != 0 ? true : false; + } + + /** + * Check if the NT server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isNTServer(int typ) + { + return (typ & NTServer) != 0 ? true : false; + } + + /** + * Check if the WFW server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isWFWServer(int typ) + { + return (typ & WfwServer) != 0 ? true : false; + } + + /** + * Check if the MFPN server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isMFPNServer(int typ) + { + return (typ & MFPNServer) != 0 ? true : false; + } + + /** + * Check if the NT non-domain controller server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isNTNonDomainServer(int typ) + { + return (typ & NTNonDCServer) != 0 ? true : false; + } + + /** + * Check if the potential browse master flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isPotentialBrowseMaster(int typ) + { + return (typ & PotentialBrowse) != 0 ? true : false; + } + + /** + * Check if the backup browser flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isBackupBrowser(int typ) + { + return (typ & BackupBrowser) != 0 ? true : false; + } + + /** + * Check if the browse master flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isBrowserMaster(int typ) + { + return (typ & MasterBrowser) != 0 ? true : false; + } + + /** + * Check if the domain master flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isDomainMaster(int typ) + { + return (typ & DomainMaster) != 0 ? true : false; + } + + /** + * Check if the OSF server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isOSFServer(int typ) + { + return (typ & OSFServer) != 0 ? true : false; + } + + /** + * Check if the VMS server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isVMSServer(int typ) + { + return (typ & VMSServer) != 0 ? true : false; + } + + /** + * Check if the Win95 plus flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isWin95Plus(int typ) + { + return (typ & Win95Plus) != 0 ? true : false; + } + + /** + * Check if the DFS root flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isDFSRoot(int typ) + { + return (typ & DFSRoot) != 0 ? true : false; + } + + /** + * Check if the NT cluster flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isNTCluster(int typ) + { + return (typ & NTCluster) != 0 ? true : false; + } + + /** + * Check if the terminal server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isTerminalServer(int typ) + { + return (typ & TerminalServer) != 0 ? true : false; + } + + /** + * Check if the DCE server flag is set + * + * @param typ int + * @return boolean + */ + public static final boolean isDCEServer(int typ) + { + return (typ & DCEServer) != 0 ? true : false; + } +} diff --git a/source/java/org/alfresco/filesys/smb/SharingMode.java b/source/java/org/alfresco/filesys/smb/SharingMode.java new file mode 100644 index 0000000000..1855816855 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/SharingMode.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * File Sharing Mode Class + */ +public class SharingMode +{ + + // File sharing mode constants + + public final static int NOSHARING = 0x0000; + public final static int READ = 0x0001; + public final static int WRITE = 0x0002; + public final static int DELETE = 0x0004; + + public final static int READWRITE = READ + WRITE; +} diff --git a/source/java/org/alfresco/filesys/smb/TcpipSMB.java b/source/java/org/alfresco/filesys/smb/TcpipSMB.java new file mode 100644 index 0000000000..40d93ec69a --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/TcpipSMB.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Native TCP/IP SMB Constants Class + */ +public class TcpipSMB +{ + + // Default port for native TCP SMB + + public static final int PORT = 445; +} diff --git a/source/java/org/alfresco/filesys/smb/TransactBuffer.java b/source/java/org/alfresco/filesys/smb/TransactBuffer.java new file mode 100644 index 0000000000..87fa9ab71e --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/TransactBuffer.java @@ -0,0 +1,617 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +import org.alfresco.filesys.util.DataBuffer; + +/** + * Transact Buffer Class + *

    + * Contains the parameters and data for a transaction, transaction2 or NT transaction request. + */ +public class TransactBuffer +{ + + // Default buffer sizes + + protected static final int DefaultSetupSize = 32; + protected static final int DefaultDataSize = 8192; + protected static final int DefaultParameterSize = 64; + + // Default maximum return buffer sizes + + protected static final int DefaultMaxSetupReturn = 16; + protected static final int DefaultMaxParameterReturn = 256; + protected static final int DefaultMaxDataReturn = 65000; + + // Tree id, connection that the transaction is for + + protected int m_treeId = -1; + + // Transaction packet type and sub-function + + protected int m_type; + protected int m_func; + + // Transaction name, for Transaction2 only + + protected String m_name; + + // Setup parameters + + protected DataBuffer m_setupBuf; + + // Parameter block + + protected DataBuffer m_paramBuf; + + // Data block and read/write position + + protected DataBuffer m_dataBuf; + + // Flag to indicate if this is a multi-packet transaction + + protected boolean m_multi; + + // Unicode strings flag + + protected boolean m_unicode; + + // Maximum setup, parameter and data bytes to return + + protected int m_maxSetup = DefaultMaxSetupReturn; + protected int m_maxParam = DefaultMaxParameterReturn; + protected int m_maxData = DefaultMaxDataReturn; + + /** + * Default constructor + */ + public TransactBuffer() + { + m_setupBuf = new DataBuffer(DefaultSetupSize); + m_paramBuf = new DataBuffer(DefaultParameterSize); + m_dataBuf = new DataBuffer(DefaultDataSize); + } + + /** + * Class constructor + * + * @param scnt int + * @param pcnt int + * @param dcnt int + */ + public TransactBuffer(int scnt, int pcnt, int dcnt) + { + + // Allocate the setup parameter buffer + + if (scnt > 0) + m_setupBuf = new DataBuffer(scnt); + + // Allocate the paramater buffer + + if (pcnt > 0) + m_paramBuf = new DataBuffer(pcnt); + + // Allocate the data buffer + + if (dcnt > 0) + m_dataBuf = new DataBuffer(dcnt); + + // Multi-packet transaction + + m_multi = true; + } + + /** + * Class constructor + * + * @param cmd int + * @param scnt int + * @param pcnt int + * @param dcnt int + */ + public TransactBuffer(int cmd, int scnt, int pcnt, int dcnt) + { + + // Set the command + + setType(cmd); + + // Allocate the setup parameter buffer + + if (scnt > 0) + m_setupBuf = new DataBuffer(scnt); + + // Allocate the paramater buffer + + if (pcnt > 0) + m_paramBuf = new DataBuffer(pcnt); + + // Allocate the data buffer + + if (dcnt > 0) + m_dataBuf = new DataBuffer(dcnt); + + // Multi-packet transaction + + m_multi = true; + } + + /** + * Class constructor + * + * @param func int + * @param name String + * @param scnt int + * @param pcnt int + * @param dcnt int + */ + public TransactBuffer(int func, String name, int scnt, int pcnt, int dcnt) + { + + // Set the name, for Transaction2 + + setName(name); + + // Allocate the setup parameter buffer + + if (scnt > 0) + m_setupBuf = new DataBuffer(scnt); + + // Allocate the paramater buffer + + if (pcnt > 0) + m_paramBuf = new DataBuffer(pcnt); + + // Allocate the data buffer + + if (dcnt > 0) + m_dataBuf = new DataBuffer(dcnt); + + // Set the function code + + setFunction(func); + + // Multi-packet transaction + + m_multi = true; + } + + /** + * Class constructor + * + * @param func int + * @param scnt int + * @param pcnt int + * @param dbuf byte[] + * @param doff int + * @param dlen int + */ + public TransactBuffer(int func, int scnt, int pcnt, byte[] dbuf, int doff, int dlen) + { + + // Allocate the setup parameter buffer + + if (scnt > 0) + m_setupBuf = new DataBuffer(scnt); + + // Allocate the paramater buffer + + if (pcnt > 0) + m_paramBuf = new DataBuffer(pcnt); + + // Allocate the data buffer + + if (dbuf != null) + m_dataBuf = new DataBuffer(dbuf, doff, dlen); + + // Set the function code + + setFunction(func); + + // Multi-packet transaction + + m_multi = true; + } + + /** + * Determine if the tree id has been set + * + * @return boolean + */ + public final boolean hasTreeId() + { + return m_treeId != -1 ? true : false; + } + + /** + * Return the tree id + * + * @return int + */ + public final int getTreeId() + { + return m_treeId; + } + + /** + * Return the transaction type (from SBMSrvPacketType, either Transaction, Transaction2 or + * NTTransact) + * + * @return int + */ + public final int isType() + { + return m_type; + } + + /** + * Return the transaction function + * + * @return int + */ + public final int getFunction() + { + return m_func; + } + + /** + * Determine if the transaction has a name + * + * @return boolean + */ + public final boolean hasName() + { + return m_name != null ? true : false; + } + + /** + * Return the transaction name + * + * @return String + */ + public final String getName() + { + return m_name; + } + + /** + * Determine if this is a multi-packet transaction + * + * @return boolean + */ + public final boolean isMultiPacket() + { + return m_multi; + } + + /** + * Determine if the client is using Unicode strings + * + * @return boolean + */ + public final boolean isUnicode() + { + return m_unicode; + } + + /** + * Determine if the transaction buffer has setup data + * + * @return boolean + */ + public final boolean hasSetupBuffer() + { + return m_setupBuf != null ? true : false; + } + + /** + * Return the setup parameter buffer + * + * @return DataBuffer + */ + public final DataBuffer getSetupBuffer() + { + return m_setupBuf; + } + + /** + * Determine if the transaction buffer has parameter data + * + * @return boolean + */ + public final boolean hasParameterBuffer() + { + return m_paramBuf != null ? true : false; + } + + /** + * Return the parameter buffer + * + * @return DataBuffer + */ + public final DataBuffer getParameterBuffer() + { + return m_paramBuf; + } + + /** + * Determine if the transaction buffer has a data block + * + * @return boolean + */ + public final boolean hasDataBuffer() + { + return m_dataBuf != null ? true : false; + } + + /** + * Return the data buffer + * + * @return DataBuffer + */ + public final DataBuffer getDataBuffer() + { + return m_dataBuf; + } + + /** + * Return the setup return data limit + * + * @return int + */ + public final int getReturnSetupLimit() + { + return m_maxSetup; + } + + /** + * Return the parameter return data limit + * + * @return int + */ + public final int getReturnParameterLimit() + { + return m_maxParam; + } + + /** + * Return the data return data limit + * + * @return int + */ + public final int getReturnDataLimit() + { + return m_maxData; + } + + /** + * Set the tree id + * + * @param tid int + */ + public final void setTreeId(int tid) + { + m_treeId = tid; + } + + /** + * Set the transaction type + * + * @param typ int + */ + public final void setType(int typ) + { + m_type = typ; + } + + /** + * Set the transaction function + * + * @param func int + */ + public final void setFunction(int func) + { + m_func = func; + } + + /** + * Set the transaction name, for Transactin2 + * + * @param name String + */ + public final void setName(String name) + { + m_name = name; + } + + /** + * Set the Unicode strings flag + * + * @param uni boolean + */ + public final void setUnicode(boolean uni) + { + m_unicode = uni; + } + + /** + * Set the limit of returned setup bytes + * + * @param limit int + */ + public final void setReturnSetupLimit(int limit) + { + m_maxSetup = limit; + } + + /** + * Set the limit of returned parameter bytes + * + * @param limit int + */ + public final void setReturnParameterLimit(int limit) + { + m_maxParam = limit; + } + + /** + * Set the limit of returned data bytes + * + * @param limit int + */ + public final void setReturnDataLimit(int limit) + { + m_maxData = limit; + } + + /** + * Set the setup, parameter and data return data limits + * + * @param slimit int + * @param plimit int + * @param dlimit int + */ + public final void setReturnLimits(int slimit, int plimit, int dlimit) + { + setReturnSetupLimit(slimit); + setReturnParameterLimit(plimit); + setReturnDataLimit(dlimit); + } + + /** + * Set the end of buffer positions for the setup, parameter and data buffers ready for reading + * the data. + */ + public final void setEndOfBuffer() + { + + // Set the end of the setup buffer + + if (m_setupBuf != null) + m_setupBuf.setEndOfBuffer(); + + // Set the end of the parameter buffer + + if (m_paramBuf != null) + m_paramBuf.setEndOfBuffer(); + + // Set the end of the data buffer + + if (m_dataBuf != null) + m_dataBuf.setEndOfBuffer(); + } + + /** + * Append setup data to the setup data buffer + * + * @param buf byte[] + * @param off int + * @param len int + */ + public final void appendSetup(byte[] buf, int off, int len) + { + m_setupBuf.appendData(buf, off, len); + } + + /** + * Append parameter data to the parameter data buffer + * + * @param buf byte[] + * @param off int + * @param len int + */ + public final void appendParameter(byte[] buf, int off, int len) + { + m_paramBuf.appendData(buf, off, len); + } + + /** + * Append data to the data buffer + * + * @param buf byte[] + * @param off int + * @param len int + */ + public final void appendData(byte[] buf, int off, int len) + { + m_dataBuf.appendData(buf, off, len); + } + + /** + * Return the transaction buffer details as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + + switch (isType()) + { + case PacketType.Transaction: + str.append("Trans"); + break; + case PacketType.Transaction2: + str.append("Trans2("); + str.append(getName()); + str.append(")"); + break; + case PacketType.NTTransact: + str.append("NTTrans"); + break; + default: + str.append("Unknown"); + break; + } + str.append("-0x"); + str.append(Integer.toHexString(getFunction())); + + str.append(": setup="); + if (m_setupBuf != null) + str.append(m_setupBuf); + else + str.append("none"); + + str.append(",param="); + if (m_paramBuf != null) + str.append(m_paramBuf); + else + str.append("none"); + + str.append(",data="); + if (m_dataBuf != null) + str.append(m_dataBuf); + else + str.append("none"); + str.append("]"); + + str.append(",max="); + str.append(getReturnSetupLimit()); + + str.append("/"); + str.append(getReturnParameterLimit()); + + str.append("/"); + str.append(getReturnDataLimit()); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/TransactionNames.java b/source/java/org/alfresco/filesys/smb/TransactionNames.java new file mode 100644 index 0000000000..5d103b2c50 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/TransactionNames.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * List of the available transaction names. + */ +public class TransactionNames +{ + + // Available transaction names + + public static final String PipeLanman = "\\PIPE\\LANMAN"; + public static final String MailslotBrowse = "\\MAILSLOT\\BROWSE"; +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/WinNT.java b/source/java/org/alfresco/filesys/smb/WinNT.java new file mode 100644 index 0000000000..830b4b5ed6 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/WinNT.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb; + +/** + * Windows NT Constants Class + */ +public class WinNT +{ + + // Compression format types + + public final static int CompressionFormatNone = 0; + public final static int CompressionFormatDefault = 1; + public final static int CompressionFormatLZNT1 = 2; + + // Get/set security descriptor flags + + public final static int SecurityOwner = 0x0001; + public final static int SecurityGroup = 0x0002; + public final static int SecurityDACL = 0x0004; + public final static int SecuritySACL = 0x0008; + + // Security impersonation levels + + public static final int SecurityAnonymous = 0; + public static final int SecurityIdentification = 1; + public static final int SecurityImpersonation = 2; + public static final int SecurityDelegation = 3; + + // Security flags + + public static final int SecurityContextTracking = 0x00040000; + public static final int SecurityEffectiveOnly = 0x00080000; + + // NTCreateAndX flags (oplocks/target) + + public static final int RequestOplock = 0x0002; + public static final int RequestBatchOplock = 0x0004; + public static final int TargetDirectory = 0x0008; + public static final int ExtendedResponse = 0x0010; + + // NTCreateAndX create options flags + + public static final int CreateFile = 0x00000000; + public static final int CreateDirectory = 0x00000001; + public static final int CreateWriteThrough = 0x00000002; + public static final int CreateSequential = 0x00000004; + + public static final int CreateNonDirectory = 0x00000040; + public static final int CreateRandomAccess = 0x00000800; + public static final int CreateDeleteOnClose = 0x00001000; +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEBuffer.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEBuffer.java new file mode 100644 index 0000000000..e828e31cd8 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEBuffer.java @@ -0,0 +1,1896 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +import org.alfresco.filesys.smb.NTTime; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.TransactBuffer; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.HexDump; + +/** + * DCE Buffer Class + */ +public class DCEBuffer +{ + + // Header value types + + public static final int HDR_VERMAJOR = 0; + public static final int HDR_VERMINOR = 1; + public static final int HDR_PDUTYPE = 2; + public static final int HDR_FLAGS = 3; + public static final int HDR_DATAREP = 4; + public static final int HDR_FRAGLEN = 5; + public static final int HDR_AUTHLEN = 6; + public static final int HDR_CALLID = 7; + public static final int HDR_ALLOCHINT = 8; + public static final int HDR_OPCODE = 9; + + // Header flags + + public static final int FLG_FIRSTFRAG = 0x01; + public static final int FLG_LASTFRAG = 0x02; + public static final int FLG_CANCEL = 0x04; + public static final int FLG_IDEMPOTENT = 0x20; + public static final int FLG_BROADCAST = 0x40; + + public static final int FLG_ONLYFRAG = 0x03; + + // DCE/RPC header offsets + + public static final int VERSIONMAJOR = 0; + public static final int VERSIONMINOR = 1; + public static final int PDUTYPE = 2; + public static final int HEADERFLAGS = 3; + public static final int PACKEDDATAREP = 4; + public static final int FRAGMENTLEN = 8; + public static final int AUTHLEN = 10; + public static final int CALLID = 12; + public static final int DCEDATA = 16; + + // DCE/RPC Request offsets + + public static final int ALLOCATIONHINT = 16; + public static final int PRESENTIDENT = 20; + public static final int OPERATIONID = 22; + public static final int OPERATIONDATA = 24; + + // DCE/RPC header constants + + private static final byte VAL_VERSIONMAJOR = 5; + private static final byte VAL_VERSIONMINOR = 0; + private static final int VAL_PACKEDDATAREP = 0x00000010; + + // Data alignment types + + public final static int ALIGN_NONE = -1; + public final static int ALIGN_SHORT = 0; + public final static int ALIGN_INT = 1; + public final static int ALIGN_LONG = 2; + + // Maximum string length + + public final static int MAX_STRING_LEN = 1000; + + // Alignment masks and rounding + + private final static int[] _alignMask = { 0xFFFFFFFE, 0xFFFFFFFC, 0xFFFFFFF8 }; + private final static int[] _alignRound = { 1, 3, 7 }; + + // Default buffer allocation + + private static final int DEFAULT_BUFSIZE = 8192; + + // Maximum buffer size, used when the buffer is reset to release large buffers + + private static final int MAX_BUFFER_SIZE = 65536; + + // Dummy address value to use for pointers within the buffer + + private static final int DUMMY_ADDRESS = 0x12345678; + + // Data buffer and current read/write positions + + private byte[] m_buffer; + private int m_base; + private int m_pos; + private int m_rdpos; + + // Error status + + private int m_errorCode; + + /** + * Default constructor + */ + public DCEBuffer() + { + m_buffer = new byte[DEFAULT_BUFSIZE]; + m_pos = 0; + m_rdpos = 0; + m_base = 0; + } + + /** + * Class constructor + * + * @param siz int + */ + public DCEBuffer(int siz) + { + m_buffer = new byte[siz]; + m_pos = 0; + m_rdpos = 0; + m_base = 0; + } + + /** + * Class constructor + * + * @param buf byte[] + * @param startPos int + * @param len int + */ + public DCEBuffer(byte[] buf, int startPos, int len) + { + m_buffer = buf; + m_pos = startPos + len; + m_rdpos = startPos; + m_base = startPos; + } + + /** + * Class constructor + * + * @param buf byte[] + * @param startPos int + */ + public DCEBuffer(byte[] buf, int startPos) + { + m_buffer = buf; + m_pos = startPos; + m_rdpos = startPos; + m_base = startPos; + } + + /** + * Class constructor + * + * @param tbuf TransactBuffer + */ + public DCEBuffer(TransactBuffer tbuf) + { + DataBuffer dataBuf = tbuf.getDataBuffer(); + m_buffer = dataBuf.getBuffer(); + m_rdpos = dataBuf.getOffset(); + m_base = dataBuf.getOffset(); + m_pos = m_rdpos + dataBuf.getLength(); + } + + /** + * Return the DCE buffer + * + * @return byte[] + */ + public final byte[] getBuffer() + { + return m_buffer; + } + + /** + * Return the current used buffer length + * + * @return int + */ + public final int getLength() + { + return m_pos; + } + + /** + * Return the read buffer position + * + * @return int + */ + public final int getReadPosition() + { + return m_rdpos; + } + + /** + * Return the write buffer position + * + * @return int + */ + public final int getWritePosition() + { + return m_pos; + } + + /** + * Return the amount of data left to read + * + * @return int + */ + public final int getAvailableLength() + { + return m_pos - m_rdpos; + } + + /** + * Get a byte from the buffer + * + * @param align int + * @return int + * @exception DCEBufferException + */ + public final int getByte(int align) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 1) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the integer value + + int bval = (int) (m_buffer[m_rdpos++] & 0xFF); + alignRxPosition(align); + return bval; + } + + /** + * Get a block of bytes from the buffer + * + * @param buf byte[] + * @param len int + * @return byte[] + * @throws DCEBufferException + */ + public final byte[] getBytes(byte[] buf, int len) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < len) + throw new DCEBufferException("End of DCE buffer"); + + // Check if a return buffer should be allocated + + if (buf == null) + buf = new byte[len]; + + // Unpack the bytes + + for (int i = 0; i < len; i++) + buf[i] = m_buffer[m_rdpos++]; + return buf; + } + + /** + * Get a short from the buffer + * + * @return int + * @exception DCEBufferException + */ + public final int getShort() throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 2) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the integer value + + int sval = (int) DataPacker.getIntelShort(m_buffer, m_rdpos); + m_rdpos += 2; + return sval; + } + + /** + * Get a short from the buffer and align the read pointer + * + * @param align int + * @return int + * @exception DCEBufferException + */ + public final int getShort(int align) throws DCEBufferException + { + + // Read the short + + int sval = getShort(); + + // Align the read position + + alignRxPosition(align); + + // Return the short value + + return sval; + } + + /** + * Get an integer from the buffer + * + * @return int + * @exception DCEBufferException + */ + public final int getInt() throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 4) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the integer value + + int ival = DataPacker.getIntelInt(m_buffer, m_rdpos); + m_rdpos += 4; + return ival; + } + + /** + * Get a pointer from the buffer + * + * @return int + * @exception DCEBufferException + */ + public final int getPointer() throws DCEBufferException + { + return getInt(); + } + + /** + * Get a pointer from the buffer and return either an empty string if the pointer is valid or + * null. + * + * @return String + * @exception DCEBufferException + */ + public final String getStringPointer() throws DCEBufferException + { + if (getInt() == 0) + return null; + return ""; + } + + /** + * Get a character array header from the buffer and return either an empty string if the pointer + * is valid or null. + * + * @return String + * @exception DCEBufferException + */ + public final String getCharArrayPointer() throws DCEBufferException + { + + // Get the array length and size + + int len = getShort(); + int siz = getShort(); + return getStringPointer(); + } + + /** + * Get a character array from the buffer if the String variable is not null, and align on the + * specified boundary + * + * @param strVar String + * @param align int + * @return String + * @exception DCEBufferException + */ + public final String getCharArrayNotNull(String strVar, int align) throws DCEBufferException + { + + // Check if the string variable is not null + + String str = ""; + + if (strVar != null) + { + + // Read the string + + str = getCharArray(); + + // Align the read position + + alignRxPosition(align); + } + + // Return the string + + return str; + } + + /** + * Get a long (64 bit) value from the buffer + * + * @return long + * @exception DCEBufferException + */ + public final long getLong() throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 8) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the integer value + + long lval = DataPacker.getIntelLong(m_buffer, m_rdpos); + m_rdpos += 8; + return lval; + } + + /** + * Return a DCE/RPC header value + * + * @param valTyp int + * @return int + */ + public final int getHeaderValue(int valTyp) + { + + int result = -1; + + switch (valTyp) + { + + // Version major + + case HDR_VERMAJOR: + result = (int) (m_buffer[m_base + VERSIONMAJOR] & 0xFF); + break; + + // Version minor + + case HDR_VERMINOR: + result = (int) (m_buffer[m_base + VERSIONMINOR] & 0xFF); + break; + + // PDU type + + case HDR_PDUTYPE: + result = (int) (m_buffer[m_base + PDUTYPE] & 0xFF); + break; + + // Flags + + case HDR_FLAGS: + result = (int) (m_buffer[m_base + HEADERFLAGS] & 0xFF); + break; + + // Data representation + + case HDR_DATAREP: + result = DataPacker.getIntelInt(m_buffer, m_base + VERSIONMINOR); + break; + + // Authorisation length + + case HDR_AUTHLEN: + result = DataPacker.getIntelInt(m_buffer, m_base + AUTHLEN); + break; + + // Fragment length + + case HDR_FRAGLEN: + result = DataPacker.getIntelInt(m_buffer, m_base + FRAGMENTLEN); + break; + + // Call id + + case HDR_CALLID: + result = DataPacker.getIntelInt(m_buffer, m_base + CALLID); + break; + + // Request allocation hint + + case HDR_ALLOCHINT: + result = DataPacker.getIntelInt(m_buffer, m_base + ALLOCATIONHINT); + break; + + // Request opcode + + case HDR_OPCODE: + result = DataPacker.getIntelShort(m_buffer, m_base + OPERATIONID); + break; + } + + // Return the header value + + return result; + } + + /** + * Set a DCE/RPC header value + * + * @param typ int + * @param val int + */ + public final void setHeaderValue(int typ, int val) + { + + switch (typ) + { + + // Version major + + case HDR_VERMAJOR: + m_buffer[m_base + VERSIONMAJOR] = (byte) (val & 0xFF); + break; + + // Version minor + + case HDR_VERMINOR: + m_buffer[m_base + VERSIONMINOR] = (byte) (val & 0xFF); + break; + + // PDU type + + case HDR_PDUTYPE: + m_buffer[m_base + PDUTYPE] = (byte) (val & 0xFF); + break; + + // Flags + + case HDR_FLAGS: + m_buffer[m_base + HEADERFLAGS] = (byte) (val & 0xFF); + break; + + // Data representation + + case HDR_DATAREP: + DataPacker.putIntelInt(val, m_buffer, m_base + PACKEDDATAREP); + break; + + // Authorisation length + + case HDR_AUTHLEN: + DataPacker.putIntelInt(val, m_buffer, m_base + AUTHLEN); + break; + + // Fragment length + + case HDR_FRAGLEN: + DataPacker.putIntelInt(val, m_buffer, m_base + FRAGMENTLEN); + break; + + // Call id + + case HDR_CALLID: + DataPacker.putIntelInt(val, m_buffer, m_base + CALLID); + break; + + // Request allocation hint + + case HDR_ALLOCHINT: + DataPacker.putIntelInt(val, m_buffer, m_base + ALLOCATIONHINT); + break; + + // Request opcode + + case HDR_OPCODE: + DataPacker.putIntelShort(val, m_buffer, m_base + OPERATIONID); + break; + } + } + + /** + * Determine if this is the first fragment + * + * @return boolean + */ + public final boolean isFirstFragment() + { + if ((getHeaderValue(HDR_FLAGS) & FLG_FIRSTFRAG) != 0) + return true; + return false; + } + + /** + * Determine if this is the last fragment + * + * @return boolean + */ + public final boolean isLastFragment() + { + if ((getHeaderValue(HDR_FLAGS) & FLG_LASTFRAG) != 0) + return true; + return false; + } + + /** + * Determine if this is the only fragment in the request + * + * @return boolean + */ + public final boolean isOnlyFragment() + { + if ((getHeaderValue(HDR_FLAGS) & FLG_ONLYFRAG) == FLG_ONLYFRAG) + return true; + return false; + } + + /** + * Check if the status indicates that there are more entries available + * + * @return boolean + */ + public final boolean hasMoreEntries() + { + return getStatusCode() == SMBStatus.Win32MoreEntries ? true : false; + } + + /** + * Check if the status indicates success + * + * @return boolean + */ + public final boolean hasSuccessStatus() + { + return getStatusCode() == SMBStatus.NTSuccess ? true : false; + } + + /** + * Skip over a number of bytes + * + * @param cnt int + * @exception DCEBufferException + */ + public final void skipBytes(int cnt) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < cnt) + throw new DCEBufferException("End of DCE buffer"); + + // Skip bytes + + m_rdpos += cnt; + } + + /** + * Skip over a pointer + * + * @exception DCEBufferException + */ + public final void skipPointer() throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 4) + throw new DCEBufferException("End of DCE buffer"); + + // Skip the 32bit pointer value + + m_rdpos += 4; + } + + /** + * Set the read position + * + * @param pos int + * @exception DCEBufferException + */ + public final void positionAt(int pos) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length < pos) + throw new DCEBufferException("End of DCE buffer"); + + // Set the read position + + m_rdpos = pos; + } + + /** + * Get a number of Unicode characters from the buffer and return as a string + * + * @param len int + * @return String + * @exception DCEBufferException + */ + public final String getChars(int len) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < (len * 2)) + throw new DCEBufferException("End of DCE buffer"); + + // Build up the return string + + StringBuffer str = new StringBuffer(len); + char curChar; + + while (len-- > 0) + { + + // Get a Unicode character from the buffer + + curChar = (char) ((m_buffer[m_rdpos + 1] << 8) + m_buffer[m_rdpos]); + m_rdpos += 2; + + // Add the character to the string + + str.append(curChar); + } + + // Return the string + + return str.toString(); + } + + /** + * Get the status code from the end of the data block + * + * @return int + */ + public final int getStatusCode() + { + + // Read the integer value at the end of the buffer + + int ival = DataPacker.getIntelInt(m_buffer, m_pos - 4); + return ival; + } + + /** + * Get a string from the buffer + * + * @return String + * @exception DCEBufferException + */ + public final String getString() throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 12) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the string + + int maxLen = getInt(); + skipBytes(4); // offset + int strLen = getInt(); + + String str = DataPacker.getUnicodeString(m_buffer, m_rdpos, strLen); + m_rdpos += (strLen * 2); + return str; + } + + /** + * Get a character array from the buffer + * + * @return String + * @exception DCEBufferException + */ + public final String getCharArray() throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 12) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the string + + int maxLen = getInt(); + skipBytes(4); // offset + int strLen = getInt(); // in unicode chars + + String str = null; + if (strLen > 0) + { + str = DataPacker.getUnicodeString(m_buffer, m_rdpos, strLen); + m_rdpos += (strLen * 2); + } + return str; + } + + /** + * Get a character array from the buffer and align on the specified boundary + * + * @param align int + * @return String + * @exception DCEBufferException + */ + public final String getCharArray(int align) throws DCEBufferException + { + + // Read the string + + String str = getCharArray(); + + // Align the read position + + alignRxPosition(align); + + // Return the string + + return str; + } + + /** + * Get a string from the buffer and align on the specified boundary + * + * @param align int + * @return String + * @exception DCEBufferException + */ + public final String getString(int align) throws DCEBufferException + { + + // Read the string + + String str = getString(); + + // Align the read position + + alignRxPosition(align); + + // Return the string + + return str; + } + + /** + * Get a string from the buffer if the String variable is not null, and align on the specified + * boundary + * + * @param strVar String + * @param align int + * @return String + * @exception DCEBufferException + */ + public final String getStringNotNull(String strVar, int align) throws DCEBufferException + { + + // Check if the string variable is not null + + String str = ""; + + if (strVar != null) + { + + // Read the string + + str = getString(); + + // Align the read position + + alignRxPosition(align); + } + + // Return the string + + return str; + } + + /** + * Get a string from a particular position in the buffer + * + * @param pos int + * @return String + * @exception DCEBufferException + */ + public final String getStringAt(int pos) throws DCEBufferException + { + + // Check if position is within the buffer + + if (m_buffer.length < pos) + throw new DCEBufferException("Buffer offset out of range, " + pos); + + // Unpack the string + + String str = DataPacker.getUnicodeString(m_buffer, pos, MAX_STRING_LEN); + return str; + } + + /** + * Read a Unicode string header and return the string length. -1 indicates a null pointer in the + * string header. + * + * @return int + * @exception DCEBufferException + */ + public final int getUnicodeHeaderLength() throws DCEBufferException + { + + // Check if there is enough data in the buffer for the Unicode header + + if (m_buffer.length - m_rdpos < 8) + throw new DCEBufferException("End of DCE buffer"); + + // Get the string length + + int len = (int) DataPacker.getIntelShort(m_buffer, m_rdpos); + m_rdpos += 4; // skip the max length too + int ptr = DataPacker.getIntelInt(m_buffer, m_rdpos); + m_rdpos += 4; + + // Check if the pointer is valid + + if (ptr == 0) + return -1; + return len; + } + + /** + * Get a unicode string from the current position in the buffer + * + * @return String + * @exception DCEBufferException + */ + public final String getUnicodeString() throws DCEBufferException + { + + // Check if there is any buffer to read + + if (m_buffer.length - m_rdpos <= 0) + throw new DCEBufferException("No more buffer"); + + // Unpack the string + + String str = DataPacker.getUnicodeString(m_buffer, m_rdpos, MAX_STRING_LEN); + if (str != null) + m_rdpos += (str.length() * 2) + 2; + return str; + } + + /** + * Get a data block from the buffer and align on the specified boundary + * + * @param align int + * @return byte[] + * @exception DCEBufferException + */ + public final byte[] getDataBlock(int align) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 12) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the data block + + int len = getInt(); + m_rdpos += 8; // skip undoc and max_len ints + + // Copy the raw data block + + byte[] dataBlk = null; + + if (len > 0) + { + + // Allocate the data block buffer + + dataBlk = new byte[len]; + + // Copy the raw data + + System.arraycopy(m_buffer, m_rdpos, dataBlk, 0, len); + } + + // Update the buffer position and align + + m_rdpos += len; + alignRxPosition(align); + return dataBlk; + } + + /** + * Get a UUID from the buffer + * + * @param readVer boolean + * @return UUID + * @exception DCEBufferException + */ + public final UUID getUUID(boolean readVer) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + int len = UUID.UUID_LENGTH_BINARY; + if (readVer == true) + len += 4; + + if (m_buffer.length - m_rdpos < len) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the UUID + + UUID uuid = new UUID(m_buffer, m_rdpos); + m_rdpos += UUID.UUID_LENGTH_BINARY; + + if (readVer == true) + { + int ver = getInt(); + uuid.setVersion(ver); + } + + return uuid; + } + + /** + * Get an NT 64bit time value. If the value is valid then convert to a Java time value + * + * @return long + * @throws DCEBufferException + */ + public final long getNTTime() throws DCEBufferException + { + + // Get the raw NT time value + + long ntTime = getLong(); + if (ntTime == 0 || ntTime == NTTime.InfiniteTime) + return ntTime; + + // Convert the time to a Java time value + + return NTTime.toJavaDate(ntTime); + } + + /** + * Get a byte structure that has a header + * + * @param buf byte[] + * @throws DCEBufferException + */ + public final byte[] getByteStructure(byte[] buf) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < 12) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the header + + int maxLen = getInt(); + skipBytes(4); // offset + int bytLen = getInt(); + + byte[] bytBuf = buf; + if (bytBuf.length < bytLen) + bytBuf = new byte[bytLen]; + return getBytes(bytBuf, bytLen); + } + + /** + * Get a handle from the buffer + * + * @param handle PolicyHandle + * @exception DCEBufferException + */ + public final void getHandle(PolicyHandle handle) throws DCEBufferException + { + + // Check if there is enough data in the buffer + + if (m_buffer.length - m_rdpos < PolicyHandle.POLICY_HANDLE_SIZE) + throw new DCEBufferException("End of DCE buffer"); + + // Unpack the policy handle + + m_rdpos = handle.loadPolicyHandle(m_buffer, m_rdpos); + } + + /** + * Copy data from the DCE buffer to the user buffer, and update the current read position. + * + * @param buf byte[] + * @param off int + * @param cnt int + * @return int + * @exception DCEBufferException + */ + public final int copyData(byte[] buf, int off, int cnt) throws DCEBufferException + { + + // Check if there is any more data to copy + + if (m_rdpos == m_pos) + return 0; + + // Calculate the amount of data to copy + + int siz = m_pos - m_rdpos; + if (siz > cnt) + siz = cnt; + + // Copy the data to the user buffer and update the current read position + + System.arraycopy(m_buffer, m_rdpos, buf, off, siz); + m_rdpos += siz; + + // Return the amount of data copied + + return siz; + } + + /** + * Append a raw data block to the buffer + * + * @param buf byte[] + * @param off int + * @param len int + * @exception DCEBufferException + */ + public final void appendData(byte[] buf, int off, int len) throws DCEBufferException + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < len) + extendBuffer(len); + + // Copy the data to the buffer and update the current write position + + System.arraycopy(buf, off, m_buffer, m_pos, len); + m_pos += len; + } + + /** + * Append an integer to the buffer + * + * @param ival int + */ + public final void putInt(int ival) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 4) + extendBuffer(); + + // Pack the integer value + + DataPacker.putIntelInt(ival, m_buffer, m_pos); + m_pos += 4; + } + + /** + * Append a byte value to the buffer + * + * @param bval int + */ + public final void putByte(int bval) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 1) + extendBuffer(); + + // Pack the short value + + m_buffer[m_pos++] = (byte) (bval & 0xFF); + } + + /** + * Append a byte value to the buffer and align to the specified boundary + * + * @param bval byte + * @param align int + */ + public final void putByte(byte bval, int align) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 1) + extendBuffer(); + + // Pack the short value + + m_buffer[m_pos++] = bval; + alignPosition(align); + } + + /** + * Append a byte value to the buffer and align to the specified boundary + * + * @param bval int + * @param align int + */ + public final void putByte(int bval, int align) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 1) + extendBuffer(); + + // Pack the short value + + m_buffer[m_pos++] = (byte) (bval & 0xFF); + alignPosition(align); + } + + /** + * Append a block of bytes to the buffer + * + * @param bval byte[] + * @param len int + */ + public final void putBytes(byte[] bval, int len) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < len) + extendBuffer(); + + // Pack the bytes + + for (int i = 0; i < len; i++) + m_buffer[m_pos++] = bval[i]; + } + + /** + * Append a block of bytes to the buffer + * + * @param bval byte[] + * @param len int + * @param align int + */ + public final void putBytes(byte[] bval, int len, int align) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < len) + extendBuffer(); + + // Pack the bytes + + for (int i = 0; i < len; i++) + m_buffer[m_pos++] = bval[i]; + + // Align the new buffer position + + alignPosition(align); + } + + /** + * Append a short value to the buffer + * + * @param sval int + */ + public final void putShort(int sval) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 2) + extendBuffer(); + + // Pack the short value + + DataPacker.putIntelShort(sval, m_buffer, m_pos); + m_pos += 2; + } + + /** + * Append a DCE string to the buffer + * + * @param str String + */ + public final void putString(String str) + { + + // Check if there is enough space in the buffer + + int reqLen = (str.length() * 2) + 24; + + if (m_buffer.length - m_pos < reqLen) + extendBuffer(reqLen); + + // Pack the string + + m_pos = DCEDataPacker.putDCEString(m_buffer, m_pos, str, false); + } + + /** + * Append a DCE string to the buffer and align to the specified boundary + * + * @param str String + * @param align int + */ + public final void putString(String str, int align) + { + + // Check if there is enough space in the buffer + + int reqLen = (str.length() * 2) + 24; + + if (m_buffer.length - m_pos < reqLen) + extendBuffer(reqLen); + + // Pack the string + + m_pos = DCEDataPacker.putDCEString(m_buffer, m_pos, str, false); + + // Align the new buffer position + + alignPosition(align); + } + + /** + * Append a DCE string to the buffer, specify whether the nul is included in the string length + * or not + * + * @param str String + * @param align int + * @param incNul boolean + */ + public final void putString(String str, int align, boolean incNul) + { + + // Check if there is enough space in the buffer + + int reqLen = (str.length() * 2) + 24; + if (incNul) + reqLen += 2; + + if (m_buffer.length - m_pos < reqLen) + extendBuffer(reqLen); + + // Pack the string + + m_pos = DCEDataPacker.putDCEString(m_buffer, m_pos, str, incNul); + + // Align the new buffer position + + alignPosition(align); + } + + /** + * Append string return buffer details. Some DCE/RPC requests incorrectly send output parameters + * as input. + * + * @param len int + * @param align int + */ + public final void putStringReturn(int len, int align) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 20) + extendBuffer(); + + // Pack the string return details + + DataPacker.putIntelInt(len, m_buffer, m_pos); + DataPacker.putZeros(m_buffer, m_pos + 4, 8); + DataPacker.putIntelInt(DUMMY_ADDRESS, m_buffer, m_pos + 12); + m_pos += 16; + + // Align the new buffer position + + alignPosition(align); + } + + /** + * Append a DCE string header to the buffer + * + * @param str String + * @param incNul boolean + */ + public final void putUnicodeHeader(String str, boolean incNul) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 8) + extendBuffer(); + + // Calculate the string length in bytes + + int sLen = 0; + if (str != null) + sLen = str.length() * 2; + + // Pack the string header + + if (str != null) + DataPacker.putIntelShort(incNul ? sLen + 2 : sLen, m_buffer, m_pos); + else + DataPacker.putIntelShort(0, m_buffer, m_pos); + + DataPacker.putIntelShort(sLen != 0 ? sLen + 2 : 0, m_buffer, m_pos + 2); + DataPacker.putIntelInt(str != null ? DUMMY_ADDRESS : 0, m_buffer, m_pos + 4); + + m_pos += 8; + } + + /** + * Append a Unicode return string header to the buffer. Some DCE/RPC requests incorrectly send + * output parameters as input. + * + * @param len int + */ + public final void putUnicodeReturn(int len) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 8) + extendBuffer(); + + // Pack the string header + + DataPacker.putIntelShort(0, m_buffer, m_pos); + DataPacker.putIntelShort(len, m_buffer, m_pos + 2); + DataPacker.putIntelInt(DUMMY_ADDRESS, m_buffer, m_pos + 4); + + m_pos += 8; + } + + /** + * Append a DCE string header to the buffer + * + * @param len int + * @param incNul boolean + */ + public final void putUnicodeHeader(int len) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 8) + extendBuffer(); + + // Calculate the string length in bytes + + int sLen = len * 2; + + // Pack the string header + + DataPacker.putIntelShort(sLen, m_buffer, m_pos); + DataPacker.putIntelShort(sLen + 2, m_buffer, m_pos + 2); + DataPacker.putIntelInt(sLen != 0 ? DUMMY_ADDRESS : 0, m_buffer, m_pos + 4); + + m_pos += 8; + } + + /** + * Append an ASCII string to the DCE buffer + * + * @param str String + * @param incNul boolean + */ + public final void putASCIIString(String str, boolean incNul) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < (str.length() + 1)) + extendBuffer(str.length() + 2); + + // Pack the string + + m_pos = DataPacker.putString(str, m_buffer, m_pos, incNul); + } + + /** + * Append an ASCII string to the DCE buffer, and align on the specified boundary + * + * @param str String + * @param incNul boolean + * @param align int + */ + public final void putASCIIString(String str, boolean incNul, int align) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < (str.length() + 1)) + extendBuffer(str.length() + 8); + + // Pack the string + + m_pos = DataPacker.putString(str, m_buffer, m_pos, incNul); + + // Align the buffer position + + alignPosition(align); + } + + /** + * Append a pointer to the buffer. + * + * @param obj Object + */ + public final void putPointer(Object obj) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 4) + extendBuffer(); + + // Check if the object is valid, if not then put a null pointer into the buffer + + if (obj == null) + DataPacker.putZeros(m_buffer, m_pos, 4); + else + DataPacker.putIntelInt(DUMMY_ADDRESS, m_buffer, m_pos); + m_pos += 4; + } + + /** + * Append a pointer to the buffer. + * + * @param notNull boolean + */ + public final void putPointer(boolean notNull) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 4) + extendBuffer(); + + // Check if the object is valid, if not then put a null pointer into the buffer + + if (notNull == false) + DataPacker.putZeros(m_buffer, m_pos, 4); + else + DataPacker.putIntelInt(DUMMY_ADDRESS, m_buffer, m_pos); + m_pos += 4; + } + + /** + * Append a UUID to the buffer + * + * @param uuid UUID + * @param writeVer boolean + */ + public final void putUUID(UUID uuid, boolean writeVer) + { + + // Check if there is enough space in the buffer + + int len = UUID.UUID_LENGTH_BINARY; + if (writeVer == true) + len += 4; + + if (m_buffer.length - m_pos < len) + extendBuffer(); + + // Pack the UUID + + m_pos = uuid.storeUUID(m_buffer, m_pos, writeVer); + } + + /** + * Append a policy handle to the buffer + * + * @param handle PolicyHandle + */ + public final void putHandle(PolicyHandle handle) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < PolicyHandle.POLICY_HANDLE_SIZE) + extendBuffer(PolicyHandle.POLICY_HANDLE_SIZE); + + // Pack the policy handle + + m_pos = handle.storePolicyHandle(m_buffer, m_pos); + } + + /** + * Append a DCE buffer to the current DCE buffer + * + * @param buf DCEBuffer + */ + public final void putBuffer(DCEBuffer buf) + { + try + { + appendData(buf.getBuffer(), buf.getReadPosition(), buf.getLength()); + } + catch (DCEBufferException ex) + { + } + } + + /** + * Append an error status to the buffer, also sets the error status value + * + * @param sts int + */ + public final void putErrorStatus(int sts) + { + + // Check if there is enough space in the buffer + + if (m_buffer.length - m_pos < 4) + extendBuffer(); + + // Pack the status value + + DataPacker.putIntelInt(sts, m_buffer, m_pos); + m_pos += 4; + + // Save the status value + + m_errorCode = sts; + } + + /** + * Append a DCE header to the buffer + * + * @param pdutyp int + * @param callid int + */ + public final void putHeader(int pdutyp, int callid) + { + m_buffer[m_pos++] = VAL_VERSIONMAJOR; + m_buffer[m_pos++] = VAL_VERSIONMINOR; + m_buffer[m_pos++] = (byte) (pdutyp & 0xFF); + m_buffer[m_pos++] = 0; + + DataPacker.putIntelInt(VAL_PACKEDDATAREP, m_buffer, m_pos); + m_pos += 4; + + DataPacker.putZeros(m_buffer, m_pos, 4); + m_pos += 4; + + DataPacker.putIntelInt(callid, m_buffer, m_pos); + m_pos += 4; + } + + /** + * Append a bind header to the buffer + * + * @param callid int + */ + public final void putBindHeader(int callid) + { + putHeader(DCECommand.BIND, callid); + } + + /** + * Append a bind acknowlegde header to the buffer + * + * @param callid int + */ + public final void putBindAckHeader(int callid) + { + putHeader(DCECommand.BINDACK, callid); + } + + /** + * Append a request header to the buffer + * + * @param callid int + * @param opcode int + * @param allocHint int + */ + public final void putRequestHeader(int callid, int opcode, int allocHint) + { + putHeader(DCECommand.REQUEST, callid); + DataPacker.putIntelInt(allocHint, m_buffer, m_pos); + m_pos += 4; + DataPacker.putZeros(m_buffer, m_pos, 2); + m_pos += 2; + DataPacker.putIntelShort(opcode, m_buffer, m_pos); + m_pos += 2; + } + + /** + * Append a response header to the buffer + * + * @param callid int + * @param allocHint int + */ + public final void putResponseHeader(int callid, int allocHint) + { + putHeader(DCECommand.RESPONSE, callid); + DataPacker.putIntelInt(allocHint, m_buffer, m_pos); + m_pos += 4; + DataPacker.putZeros(m_buffer, m_pos, 4); + m_pos += 4; + } + + /** + * Append zero integers to the buffer + * + * @param cnt int + */ + public final void putZeroInts(int cnt) + { + + // Check if there is enough space in the buffer + + int bytCnt = cnt * 4; + if (m_buffer.length - m_pos < bytCnt) + extendBuffer(bytCnt * 2); + + // Pack the zero integer values + + DataPacker.putZeros(m_buffer, m_pos, bytCnt); + m_pos += bytCnt; + } + + /** + * Reset the buffer pointers to reuse the buffer + */ + public final void resetBuffer() + { + + // Reset the read/write positions + + m_pos = 0; + m_rdpos = 0; + + // If the buffer is over sized release it and allocate a standard sized buffer + + if (m_buffer.length >= MAX_BUFFER_SIZE) + m_buffer = new byte[DEFAULT_BUFSIZE]; + } + + /** + * Set the new write position + * + * @param pos int + */ + public final void setWritePosition(int pos) + { + m_pos = pos; + } + + /** + * Update the write position by the specified amount + * + * @param len int + */ + public final void updateWritePosition(int len) + { + m_pos += len; + } + + /** + * Determine if there is an error status set + * + * @return boolean + */ + public final boolean hasErrorStatus() + { + return m_errorCode != 0 ? true : false; + } + + /** + * Return the error status code + * + * @return int + */ + public final int getErrorStatus() + { + return m_errorCode; + } + + /** + * Set the error status code + * + * @param sts int + */ + public final void setErrorStatus(int sts) + { + m_errorCode = sts; + } + + /** + * Extend the DCE buffer by the specified amount + * + * @param ext int + */ + private final void extendBuffer(int ext) + { + + // Create a new buffer of the required size + + byte[] newBuf = new byte[m_buffer.length + ext]; + + // Copy the data from the current buffer to the new buffer + + System.arraycopy(m_buffer, 0, newBuf, 0, m_buffer.length); + + // Set the new buffer to be the main buffer + + m_buffer = newBuf; + } + + /** + * Extend the DCE buffer, double the currently allocated buffer size + */ + private final void extendBuffer() + { + extendBuffer(m_buffer.length * 2); + } + + /** + * Align the current buffer position on the specified boundary + * + * @param align int + */ + private final void alignPosition(int align) + { + + // Range check the alignment + + if (align < 0 || align > 2) + return; + + // Align the buffer position on the required boundary + + m_pos = (m_pos + _alignRound[align]) & _alignMask[align]; + } + + /** + * Align the receive buffer position on the specified boundary + * + * @param align int + */ + private final void alignRxPosition(int align) + { + + // Range check the alignment + + if (align < 0 || align > 2 || m_rdpos >= m_buffer.length) + return; + + // Align the buffer position on the required boundary + + m_rdpos = (m_rdpos + _alignRound[align]) & _alignMask[align]; + } + + /** + * Dump the DCE buffered data + */ + public final void Dump() + { + int len = getLength(); + if (len == 0) + len = 24; + HexDump.Dump(getBuffer(), len, m_base); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEBufferException.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEBufferException.java new file mode 100644 index 0000000000..5e45af1f1e --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEBufferException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE Buffer Exception Class + */ +public class DCEBufferException extends Exception +{ + private static final long serialVersionUID = 3833460725724494132L; + + /** + * Class constructor + */ + public DCEBufferException() + { + super(); + } + + /** + * Class constructor + * + * @param str String + */ + public DCEBufferException(String str) + { + super(str); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCECommand.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCECommand.java new file mode 100644 index 0000000000..39691ffce8 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCECommand.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE/RPC Command Codes + */ +public class DCECommand +{ + + // DCE/RPC Packet Types + + public final static byte REQUEST = 0x00; + public final static byte RESPONSE = 0x02; + public final static byte FAULT = 0x03; + public final static byte BIND = 0x0B; + public final static byte BINDACK = 0x0C; + public final static byte ALTCONT = 0x0E; + public final static byte AUTH3 = 0x0F; + public final static byte BINDCONT = 0x10; + + /** + * Convert the command type to a string + * + * @param cmd int + * @return String + */ + public final static String getCommandString(int cmd) + { + + // Determine the PDU command type + + String ret = ""; + switch (cmd) + { + case REQUEST: + ret = "Request"; + break; + case RESPONSE: + ret = "Repsonse"; + break; + case FAULT: + ret = "Fault"; + break; + case BIND: + ret = "Bind"; + break; + case BINDACK: + ret = "BindAck"; + break; + case ALTCONT: + ret = "AltCont"; + break; + case AUTH3: + ret = "Auth3"; + break; + case BINDCONT: + ret = "BindCont"; + break; + } + return ret; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEDataPacker.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEDataPacker.java new file mode 100644 index 0000000000..aff30e75b2 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEDataPacker.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +import org.alfresco.filesys.util.DataPacker; + +/** + * DCE Data Packer Class + */ +public class DCEDataPacker +{ + + /** + * Unpack a DCE string from the buffer + * + * @param buf byte[] + * @param off int + * @return String + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static String getDCEString(byte[] buf, int off) throws IndexOutOfBoundsException + { + + // Check if the buffer is big enough to hold the String header + + if (buf.length < off + 12) + throw new IndexOutOfBoundsException(); + + // Get the maximum and actual string length + + int maxLen = DataPacker.getIntelInt(buf, off); + int strLen = DataPacker.getIntelInt(buf, off + 8); + + // Read the Unicode string + + return DataPacker.getUnicodeString(buf, off + 12, strLen); + } + + /** + * Pack a DCE string into the buffer + * + * @param buf byte[] + * @param off int + * @param str String + * @param incNul boolean + * @return int + */ + public final static int putDCEString(byte[] buf, int off, String str, boolean incNul) + { + + // Pack the string header + + DataPacker.putIntelInt(str.length() + 1, buf, off); + DataPacker.putZeros(buf, off + 4, 4); + + if (incNul == false) + DataPacker.putIntelInt(str.length(), buf, off + 8); + else + DataPacker.putIntelInt(str.length() + 1, buf, off + 8); + + // Pack the string + + return DataPacker.putUnicodeString(str, buf, off + 12, incNul); + } + + /** + * Align a buffer offset on a longword boundary + * + * @param pos int + * @return int + */ + public final static int wordAlign(int pos) + { + return (pos + 1) & 0xFFFFFFFE; + } + + /** + * Align a buffer offset on a longword boundary + * + * @param pos int + * @return int + */ + public final static int longwordAlign(int pos) + { + return (pos + 3) & 0xFFFFFFFC; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEException.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEException.java new file mode 100644 index 0000000000..88c3154bea --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE/RPC Exception Class + */ +public class DCEException extends Exception +{ + private static final long serialVersionUID = 3258688788954625072L; + + /** + * Class constructor + * + * @param str String + */ + public DCEException(String str) + { + super(str); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEList.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEList.java new file mode 100644 index 0000000000..d87e1e6fcb --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEList.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +import java.util.Vector; + +/** + * DCE/RPC List Class + *

    + * Base class for lists of objects that are DCE/RPC readable and/or writeable. + */ +public abstract class DCEList +{ + + // Information level + + private int m_infoLevel; + + // List of DCE/RPC readable/writeable objects + + private Vector m_dceObjects; + + /** + * Default constructor + */ + protected DCEList() + { + m_dceObjects = new Vector(); + } + + /** + * Class constructor + * + * @param infoLevel int + */ + protected DCEList(int infoLevel) + { + m_dceObjects = new Vector(); + m_infoLevel = infoLevel; + } + + /** + * Class constructor + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + protected DCEList(DCEBuffer buf) throws DCEBufferException + { + + // Read the header from the DCE/RPC buffer that contains the information level and container + // pointer + + m_infoLevel = buf.getInt(); + buf.skipBytes(4); + + if (buf.getPointer() != 0) + { + + // Indicate that the container is valid + + m_dceObjects = new Vector(); + } + else + { + + // Container is not valid, no more data to follow + + m_dceObjects = null; + } + } + + /** + * Return the information level + * + * @return int + */ + public final int getInformationLevel() + { + return m_infoLevel; + } + + /** + * Return the number of entries in the list + * + * @return int + */ + public final int numberOfEntries() + { + return m_dceObjects != null ? m_dceObjects.size() : 0; + } + + /** + * Return the object list + * + * @return Vector + */ + public final Vector getList() + { + return m_dceObjects; + } + + /** + * Return an element from the list + * + * @param idx int + * @return Object + */ + public final Object getElement(int idx) + { + + // Range check the index + + if (m_dceObjects == null || idx < 0 || idx >= m_dceObjects.size()) + return null; + + // Return the object + + return m_dceObjects.elementAt(idx); + } + + /** + * Determine if the container is valid + * + * @return boolean + */ + protected final boolean containerIsValid() + { + return m_dceObjects != null ? true : false; + } + + /** + * Add an object to the list + * + * @param obj Object + */ + protected final void addObject(Object obj) + { + m_dceObjects.addElement(obj); + } + + /** + * Set the information level + * + * @param infoLevel int + */ + protected final void setInformationLevel(int infoLevel) + { + m_infoLevel = infoLevel; + } + + /** + * Set the object list + * + * @param list Vector + */ + protected final void setList(Vector list) + { + m_dceObjects = list; + } + + /** + * Get a new object for the list to fill in + * + * @return DCEReadable + */ + protected abstract DCEReadable getNewObject(); + + /** + * Read a list of objects from the DCE buffer + * + * @param buf DCEBuffer + * @throws DCEBufferException + */ + public void readList(DCEBuffer buf) throws DCEBufferException + { + + // Check if the container is valid, if so the object list will be valid + + if (containerIsValid() == false) + return; + + // Read the container object count and array pointer + + int numEntries = buf.getInt(); + if (buf.getPointer() != 0) + { + + // Get the array element count + + int elemCnt = buf.getInt(); + + if (elemCnt > 0) + { + + // Read in the array elements + + while (elemCnt-- > 0) + { + + // Create a readable object and add to the list + + DCEReadable element = getNewObject(); + addObject(element); + + // Load the main object details + + element.readObject(buf); + } + + // Load the strings for the readable information objects + + for (int i = 0; i < numberOfEntries(); i++) + { + + // Get a readable object + + DCEReadable element = (DCEReadable) getList().elementAt(i); + + // Load the strings for the readable object + + element.readStrings(buf); + } + } + } + } + + /** + * Write the list of objects to a DCE buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public final void writeList(DCEBuffer buf) throws DCEBufferException + { + + // Pack the container header + + buf.putInt(getInformationLevel()); + buf.putInt(getInformationLevel()); + + // Check if the object list is valid + + if (m_dceObjects != null) + { + + // Add a pointer to the container and the number of objects + + buf.putPointer(true); + buf.putInt(m_dceObjects.size()); + + // Add the pointer to the array of objects and number of objects + + buf.putPointer(true); + buf.putInt(m_dceObjects.size()); + + // Create a seperate DCE buffer to build the string list which may follow the main + // object list, depending on the object + + DCEBuffer strBuf = new DCEBuffer(); + + // Pack the object information + + for (int i = 0; i < m_dceObjects.size(); i++) + { + + // Get an object from the list + + DCEWriteable object = (DCEWriteable) m_dceObjects.elementAt(i); + + // Write the object to the buffer, strings may go into the seperate string buffer + // which will be appended + // to the main buffer after all the objects have been written + + object.writeObject(buf, strBuf); + } + + // If the string buffer has been used append it to the main buffer + + buf.putBuffer(strBuf); + + // Add the trailing list size + + buf.putInt(m_dceObjects.size()); + + // Add the enum handle + + buf.putInt(0); + } + else + { + + // Add an empty container/array + + buf.putZeroInts(4); + } + } + + /** + * Return the list as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("[Level="); + str.append(getInformationLevel()); + str.append(",Entries="); + str.append(numberOfEntries()); + str.append(",Class="); + str.append(getNewObject().getClass().getName()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEPipeType.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEPipeType.java new file mode 100644 index 0000000000..e1b14ab34a --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEPipeType.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + *

    + * Defines the special DCE/RPC pipe names. + */ +public class DCEPipeType +{ + + // IPC$ client pipe names + + private static final String[] _pipeNames = { "\\PIPE\\srvsvc", + "\\PIPE\\samr", + "\\PIPE\\winreg", + "\\PIPE\\wkssvc", + "\\PIPE\\NETLOGON", + "\\PIPE\\lsarpc", + "\\PIPE\\spoolss", + "\\PIPE\\netdfs", + "\\PIPE\\svcctl", + "\\PIPE\\EVENTLOG", + "\\PIPE\\NETLOGON" + }; + + // IPC$ server pipe names + + private static final String[] _srvNames = { "\\PIPE\\ntsvcs", + "\\PIPE\\lsass", + "\\PIPE\\winreg", + "\\PIPE\\ntsvcs", + "\\PIPE\\lsass", + "\\PIPE\\lsass", + "\\PIPE\\spoolss", + "\\PIPE\\netdfs", + "\\PIPE\\svcctl", + "\\PIPE\\EVENTLOG" + }; + + // IPC$ pipe ids + + public static final int PIPE_SRVSVC = 0; + public static final int PIPE_SAMR = 1; + public static final int PIPE_WINREG = 2; + public static final int PIPE_WKSSVC = 3; + public static final int PIPE_NETLOGON = 4; + public static final int PIPE_LSARPC = 5; + public static final int PIPE_SPOOLSS = 6; + public static final int PIPE_NETDFS = 7; + public static final int PIPE_SVCCTL = 8; + public static final int PIPE_EVENTLOG = 9; + public static final int PIPE_NETLOGON1= 10; + + // IPC$ pipe UUIDs + + private static UUID _uuidNetLogon = new UUID("8a885d04-1ceb-11c9-9fe8-08002b104860", 2); + private static UUID _uuidWinReg = new UUID("338cd001-2244-31f1-aaaa-900038001003", 1); + private static UUID _uuidSvcCtl = new UUID("367abb81-9844-35f1-ad32-98f038001003", 2); + private static UUID _uuidLsaRpc = new UUID("12345678-1234-abcd-ef00-0123456789ab", 0); + private static UUID _uuidSrvSvc = new UUID("4b324fc8-1670-01d3-1278-5a47bf6ee188", 3); + private static UUID _uuidWksSvc = new UUID("6bffd098-a112-3610-9833-46c3f87e345a", 1); + private static UUID _uuidSamr = new UUID("12345778-1234-abcd-ef00-0123456789ac", 1); + private static UUID _uuidSpoolss = new UUID("12345778-1234-abcd-ef00-0123456789ab", 1); + private static UUID _uuidSvcctl = new UUID("367abb81-9844-35f1-ad32-98f038001003", 2); + private static UUID _uuidEventLog = new UUID("82273FDC-E32A-18C3-3F78-827929DC23EA", 0); + private static UUID _uuidNetLogon1= new UUID("12345678-1234-abcd-ef00-01234567cffb", 1); + +// private static UUID _uuidAtSvc = new UUID("1ff70682-0a51-30e8-076d-740be8cee98b", 1); + + /** + * Convert a pipe name to a type + * + * @param name String + * @return int + */ + public final static int getNameAsType(String name) + { + for (int i = 0; i < _pipeNames.length; i++) + { + if (_pipeNames[i].equals(name)) + return i; + } + return -1; + } + + /** + * Convert a pipe type to a name + * + * @param typ int + * @return String + */ + public final static String getTypeAsString(int typ) + { + if (typ >= 0 && typ < _pipeNames.length) + return _pipeNames[typ]; + return null; + } + + /** + * Convert a pipe type to a short name + * + * @param typ int + * @return String + */ + public final static String getTypeAsStringShort(int typ) + { + if (typ >= 0 && typ < _pipeNames.length) + { + String name = _pipeNames[typ]; + return name.substring(5); + } + return null; + } + + /** + * Return the UUID for the pipe type + * + * @param typ int + * @return UUID + */ + public final static UUID getUUIDForType(int typ) + { + UUID ret = null; + + switch (typ) + { + case PIPE_NETLOGON: + ret = _uuidNetLogon; + break; + case PIPE_NETLOGON1: + ret = _uuidNetLogon1; + break; + case PIPE_WINREG: + ret = _uuidWinReg; + break; + case PIPE_LSARPC: + ret = _uuidLsaRpc; + break; + case PIPE_WKSSVC: + ret = _uuidWksSvc; + break; + case PIPE_SAMR: + ret = _uuidSamr; + break; + case PIPE_SRVSVC: + ret = _uuidSrvSvc; + break; + case PIPE_SPOOLSS: + ret = _uuidSpoolss; + break; + case PIPE_SVCCTL: + ret = _uuidSvcCtl; + break; + case PIPE_EVENTLOG: + ret = _uuidEventLog; + break; + } + return ret; + } + + /** + * Get the server-side pipe name for the specified pipe + * + * @param typ int + * @return String + */ + public final static String getServerPipeName(int typ) + { + if (typ >= 0 && typ < _srvNames.length) + return _srvNames[typ]; + return null; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEReadable.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEReadable.java new file mode 100644 index 0000000000..758beb8b35 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEReadable.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE/RPC Readable Interface + *

    + * A class that implements the DCEReadable interface can load itself from a DCE buffer. + */ +public interface DCEReadable +{ + + /** + * Read the object state from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readObject(DCEBuffer buf) throws DCEBufferException; + + /** + * Read the strings for object from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readStrings(DCEBuffer buf) throws DCEBufferException; +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEReadableList.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEReadableList.java new file mode 100644 index 0000000000..15b8d2cc31 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEReadableList.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE/RPC Readable List Interface + *

    + * A class that implements the DCEReadableList interface can read a list of DCEReadable objects from + * a DCE/RPC buffer. + */ +public interface DCEReadableList +{ + + /** + * Read the object state from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readObject(DCEBuffer buf) throws DCEBufferException; +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteable.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteable.java new file mode 100644 index 0000000000..a7a2fe16b5 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteable.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE/RPC Writeable Interface + *

    + * A class that implements the DCEWriteable interface can save itself to a DCE buffer. + */ +public interface DCEWriteable +{ + + /** + * Write the object state to DCE/RPC buffers. + *

    + * If a list of objects is being written the strings will be written after the objects so the + * second buffer will be specified. + *

    + * If a single object is being written to the buffer the second buffer may be null or be the + * same buffer as the main buffer. + * + * @param buf DCEBuffer + * @param strBuf DCEBuffer + * @exception DCEBufferException + */ + public void writeObject(DCEBuffer buf, DCEBuffer strBuf) throws DCEBufferException; +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteableList.java b/source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteableList.java new file mode 100644 index 0000000000..186e6d5647 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/DCEWriteableList.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * DCE/RPC Writeable List Interface + *

    + * A class that implements the DCEWriteableList interface can write a list of DCEWriteable objects + * to a DCE/RPC buffer. + */ +public interface DCEWriteableList +{ + + /** + * Write the object state to DCE/RPC buffers. + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void writeObject(DCEBuffer buf) throws DCEBufferException; +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandle.java b/source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandle.java new file mode 100644 index 0000000000..6a3d94258b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandle.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * Policy Handle Class + */ +public class PolicyHandle +{ + + // Length of a policy handle + + public static final int POLICY_HANDLE_SIZE = 20; + + // Policy handle bytes + + private byte[] m_handle; + + // Handle name + + private String m_name; + + /** + * Default constructor + */ + public PolicyHandle() + { + setName(""); + } + + /** + * Class constructor + * + * @param buf byte[] + * @param off int + */ + public PolicyHandle(byte[] buf, int off) + { + initialize(buf, off); + setName(""); + } + + /** + * Class constructor + * + * @param name String + * @param buf byte[] + * @param off int + */ + public PolicyHandle(String name, byte[] buf, int off) + { + initialize(buf, off); + setName(name); + } + + /** + * Determine if the policy handle is valid + * + * @return boolean + */ + public final boolean isValid() + { + return m_handle != null ? true : false; + } + + /** + * Return the policy handle bytes + * + * @return byte[] + */ + public final byte[] getBytes() + { + return m_handle; + } + + /** + * Return the policy handle name + * + * @return String + */ + public final String getName() + { + return m_name; + } + + /** + * Set the policy handle name + * + * @param name String + */ + public final void setName(String name) + { + m_name = name; + } + + /** + * Store the policy handle into the specified buffer + * + * @param buf byte[] + * @param off int + * @return int + */ + public final int storePolicyHandle(byte[] buf, int off) + { + + // Check if the policy handle is valid + + if (isValid() == false) + return -1; + + // Copy the policy handle bytes to the user buffer + + for (int i = 0; i < POLICY_HANDLE_SIZE; i++) + buf[off + i] = m_handle[i]; + + // Return the new buffer position + + return off + POLICY_HANDLE_SIZE; + } + + /** + * Load the policy handle from the specified buffer + * + * @param buf byte[] + * @param off int + * @return int + */ + public final int loadPolicyHandle(byte[] buf, int off) + { + + // Load the policy handle from the buffer + + initialize(buf, off); + return off + POLICY_HANDLE_SIZE; + } + + /** + * Clear the handle + */ + protected final void clearHandle() + { + m_handle = null; + } + + /** + * Initialize the policy handle + * + * @param buf byte[] + * @param off int + */ + private final void initialize(byte[] buf, int off) + { + + // Copy the policy handle bytes + + if ((off + POLICY_HANDLE_SIZE) <= buf.length) + { + + // Allocate the policy handle buffer + + m_handle = new byte[POLICY_HANDLE_SIZE]; + + // Copy the policy handle + + for (int i = 0; i < POLICY_HANDLE_SIZE; i++) + m_handle[i] = buf[off + i]; + } + } + + /** + * Return the policy handle as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + + if (getName() != null) + str.append(getName()); + str.append(":"); + + if (isValid()) + { + for (int i = 0; i < POLICY_HANDLE_SIZE; i++) + { + int val = (int) (m_handle[i] & 0xFF); + if (val <= 16) + str.append("0"); + str.append(Integer.toHexString(val).toUpperCase()); + str.append("-"); + } + str.setLength(str.length() - 1); + str.append("]"); + } + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandleCache.java b/source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandleCache.java new file mode 100644 index 0000000000..94dc9239e4 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/PolicyHandleCache.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +import java.util.Enumeration; +import java.util.Hashtable; + +/** + * Policy Handle Cache Class + */ +public class PolicyHandleCache +{ + + // Policy handles + + private Hashtable m_cache; + + /** + * Default constructor + */ + public PolicyHandleCache() + { + m_cache = new Hashtable(); + } + + /** + * Return the number of handles in the cache + * + * @return int + */ + public final int numberOfHandles() + { + return m_cache.size(); + } + + /** + * Add a handle to the cache + * + * @param name String + * @param handle PolicyHandle + */ + public final void addHandle(String name, PolicyHandle handle) + { + m_cache.put(name, handle); + } + + /** + * Return the handle for the specified index + * + * @param index String + * @return PolicyHandle + */ + public final PolicyHandle findHandle(String index) + { + return m_cache.get(index); + } + + /** + * Delete a handle from the cache + * + * @param index String + * @return PolicyHandle + */ + public final PolicyHandle removeHandle(String index) + { + return m_cache.remove(index); + } + + /** + * Enumerate the handles in the cache + * + * @return Enumeration + */ + public final Enumeration enumerateHandles() + { + return m_cache.elements(); + } + + /** + * Clear all handles from the cache + */ + public final void removeAllHandles() + { + m_cache.clear(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/Srvsvc.java b/source/java/org/alfresco/filesys/smb/dcerpc/Srvsvc.java new file mode 100644 index 0000000000..1cbde669ca --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/Srvsvc.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * Srvsvc Operation Ids Class + */ +public class Srvsvc +{ + + // Srvsvc opcodes + + public static final int NetrServerGetInfo = 0x15; + public static final int NetrServerSetInfo = 0x16; + public static final int NetrShareEnum = 0x0F; + public static final int NetrShareEnumSticky = 0x24; + public static final int NetrShareGetInfo = 0x10; + public static final int NetrShareSetInfo = 0x11; + public static final int NetrShareAdd = 0x0E; + public static final int NetrShareDel = 0x12; + public static final int NetrSessionEnum = 0x0C; + public static final int NetrSessionDel = 0x0D; + public static final int NetrConnectionEnum = 0x08; + public static final int NetrFileEnum = 0x09; + public static final int NetrRemoteTOD = 0x1C; + + /** + * Convert an opcode to a function name + * + * @param opCode int + * @return String + */ + public final static String getOpcodeName(int opCode) + { + + String ret = ""; + switch (opCode) + { + case NetrServerGetInfo: + ret = "NetrServerGetInfo"; + break; + case NetrServerSetInfo: + ret = "NetrServerSetInfo"; + break; + case NetrShareEnum: + ret = "NetrShareEnum"; + break; + case NetrShareEnumSticky: + ret = "NetrShareEnumSticky"; + break; + case NetrShareGetInfo: + ret = "NetrShareGetInfo"; + break; + case NetrShareSetInfo: + ret = "NetrShareSetInfo"; + break; + case NetrShareAdd: + ret = "NetrShareAdd"; + break; + case NetrShareDel: + ret = "NetrShareDel"; + break; + case NetrSessionEnum: + ret = "NetrSessionEnum"; + break; + case NetrSessionDel: + ret = "NetrSessionDel"; + break; + case NetrConnectionEnum: + ret = "NetrConnectionEnum"; + break; + case NetrFileEnum: + ret = "NetrFileEnum"; + break; + case NetrRemoteTOD: + ret = "NetrRemoteTOD"; + break; + } + return ret; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/UUID.java b/source/java/org/alfresco/filesys/smb/dcerpc/UUID.java new file mode 100644 index 0000000000..ee10e50df2 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/UUID.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +import org.alfresco.filesys.util.DataPacker; + +/** + * Universal Unique Identifier Class + */ +public class UUID +{ + + // UUID constants + + public static final int UUID_LENGTH = 36; + public static final int UUID_LENGTH_BINARY = 16; + private static final String UUID_VALIDCHARS = "0123456789ABCDEFabcdef"; + + // UUID string + + private String m_uuid; + + // Interface version + + private int m_ifVersion; + + // UUID bytes + + private byte[] m_uuidBytes; + + /** + * Class constructor + * + * @param id String + */ + public UUID(String id) + { + if (validateUUID(id)) + { + m_uuid = id; + m_ifVersion = 1; + } + } + + /** + * Class constructor + * + * @param id String + * @param ver int + */ + public UUID(String id, int ver) + { + if (validateUUID(id)) + { + m_uuid = id; + m_ifVersion = ver; + } + } + + /** + * Class constructor + * + * @param buf byte[] + * @param off int + */ + public UUID(byte[] buf, int off) + { + + // Copy the UUID bytes and generate the UUID string + + if ((off + UUID_LENGTH_BINARY) <= buf.length) + { + + // Take a copy of the UUID bytes + + m_uuidBytes = new byte[UUID_LENGTH_BINARY]; + for (int i = 0; i < UUID_LENGTH_BINARY; i++) + m_uuidBytes[i] = buf[off + i]; + + // Generate the string version of the UUID + + m_uuid = generateUUIDString(m_uuidBytes); + } + } + + /** + * Determine if the UUID is valid + * + * @return boolean + */ + public final boolean isValid() + { + return m_uuid != null ? true : false; + } + + /** + * Return the UUID string + * + * @return String + */ + public final String getUUID() + { + return m_uuid; + } + + /** + * Return the interface version + * + * @return int + */ + public final int getVersion() + { + return m_ifVersion; + } + + /** + * Set the interface version + * + * @param ver int + */ + public final void setVersion(int ver) + { + m_ifVersion = ver; + } + + /** + * Return the UUID as a byte array + * + * @return byte[] + */ + public final byte[] getBytes() + { + + // Check if the byte array has been created + + if (m_uuidBytes == null) + { + + // Allocate the byte array + + m_uuidBytes = new byte[UUID_LENGTH_BINARY]; + + try + { + + // Convert the first integer and pack into the buffer + + String val = m_uuid.substring(0, 8); + long lval = Long.parseLong(val, 16); + DataPacker.putIntelInt((int) (lval & 0xFFFFFFFF), m_uuidBytes, 0); + + // Convert the second word and pack into the buffer + + val = m_uuid.substring(9, 13); + int ival = Integer.parseInt(val, 16); + DataPacker.putIntelShort(ival, m_uuidBytes, 4); + + // Convert the third word and pack into the buffer + + val = m_uuid.substring(14, 18); + ival = Integer.parseInt(val, 16); + DataPacker.putIntelShort(ival, m_uuidBytes, 6); + + // Convert the fourth word and pack into the buffer + + val = m_uuid.substring(19, 23); + ival = Integer.parseInt(val, 16); + DataPacker.putShort((short) (ival & 0xFFFF), m_uuidBytes, 8); + + // Convert the final block of hex pairs to bytes + + int strPos = 24; + int bytPos = 10; + + for (int i = 0; i < 6; i++) + { + val = m_uuid.substring(strPos, strPos + 2); + m_uuidBytes[bytPos++] = (byte) (Short.parseShort(val, 16) & 0xFF); + strPos += 2; + } + } + catch (NumberFormatException ex) + { + m_uuidBytes = null; + } + } + + // Return the UUID bytes + + return m_uuidBytes; + } + + /** + * Validate a UUID string + * + * @param idStr String + * @reutrn boolean + */ + public static final boolean validateUUID(String idStr) + { + + // Check if the UUID string is the correct length + + if (idStr == null || idStr.length() != UUID_LENGTH) + return false; + + // Check for seperators + + if (idStr.charAt(8) != '-' || idStr.charAt(13) != '-' || idStr.charAt(18) != '-' || idStr.charAt(23) != '-') + return false; + + // Check for hex digits + + int i = 0; + for (i = 0; i < 8; i++) + if (UUID_VALIDCHARS.indexOf(idStr.charAt(i)) == -1) + return false; + for (i = 9; i < 13; i++) + if (UUID_VALIDCHARS.indexOf(idStr.charAt(i)) == -1) + return false; + for (i = 14; i < 18; i++) + if (UUID_VALIDCHARS.indexOf(idStr.charAt(i)) == -1) + return false; + for (i = 19; i < 23; i++) + if (UUID_VALIDCHARS.indexOf(idStr.charAt(i)) == -1) + return false; + for (i = 24; i < 36; i++) + if (UUID_VALIDCHARS.indexOf(idStr.charAt(i)) == -1) + return false; + + // Valid UUID string + + return true; + } + + /** + * Generate a UUID string from the binary representation + * + * @param buf byte[] + * @return String + */ + public static final String generateUUIDString(byte[] buf) + { + + // Build up the UUID string + + StringBuffer str = new StringBuffer(UUID_LENGTH); + + // Convert the first longword + + int ival = DataPacker.getIntelInt(buf, 0); + str.append(Integer.toHexString(ival)); + while (str.length() != 8) + str.insert(0, ' '); + str.append("-"); + + // Convert the second word + + ival = DataPacker.getIntelShort(buf, 4) & 0xFFFF; + str.append(Integer.toHexString(ival)); + while (str.length() != 13) + str.insert(9, '0'); + str.append("-"); + + // Convert the third word + + ival = DataPacker.getIntelShort(buf, 6) & 0xFFFF; + str.append(Integer.toHexString(ival)); + while (str.length() != 18) + str.insert(14, '0'); + str.append("-"); + + // Convert the remaining bytes + + for (int i = 8; i < UUID_LENGTH_BINARY; i++) + { + + // Get the current byte value and add to the string + + ival = (int) (buf[i] & 0xFF); + if (ival < 16) + str.append('0'); + str.append(Integer.toHexString(ival)); + + // Add the final seperator + + if (i == 9) + str.append("-"); + } + + // Return the UUID string + + return str.toString(); + } + + /** + * Compare a UUID with the current UUID + * + * @param id UUID + * @return boolean + */ + public final boolean compareTo(UUID id) + { + + // Compare the UUID versions + + if (getVersion() != id.getVersion()) + return false; + + // Compare the UUID bytes + + byte[] thisBytes = getBytes(); + byte[] idBytes = id.getBytes(); + + for (int i = 0; i < UUID_LENGTH_BINARY; i++) + if (thisBytes[i] != idBytes[i]) + return false; + return true; + } + + /** + * Write the binary UUID to the specified buffer, and optionally the UUID version + * + * @param buf byte[] + * @param off int + * @param writeVer boolean + * @return int + */ + public final int storeUUID(byte[] buf, int off, boolean writeVer) + { + + // Get the UUID bytes + + int pos = off; + byte[] uuidByts = getBytes(); + if (uuidByts == null) + return pos; + + // Write the binary UUID to the buffer + + for (int i = 0; i < UUID_LENGTH_BINARY; i++) + buf[pos + i] = uuidByts[i]; + pos += UUID_LENGTH_BINARY; + + // Check if version should be written to the buffer + + if (writeVer) + { + DataPacker.putIntelInt(getVersion(), buf, pos); + pos += 4; + } + + // Return the new buffer position + + return pos; + } + + /** + * Return the UUID as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(m_uuid); + str.append(":"); + str.append(m_ifVersion); + str.append("]"); + + return str.toString(); + } + + /*********************************************************************************************** + * Test Code + * + * @param args String[] + */ + /** + * public final static void main(String[] args) { System.out.println("UUID Test"); + * System.out.println("---------"); String[] uuids = { "12345678-1234-abcd-ef00-01234567cffb", + * "8a885d04-1ceb-11c9-9fe8-08002b104860", "338cd001-2244-31f1-aaaa-900038001003", + * "367abb81-9844-35f1-ad32-98f038001003", "4b324fc8-1670-01d3-1278-5a47bf6ee188", + * "6bffd098-a112-3610-9833-46c3f87e345a", "12345678-1234-abcd-ef00-0123456789ac", + * "12345778-1234-abcd-ef00-0123456789ab", "1ff70682-0a51-30e8-076d-740be8cee98b" }; // Validate + * and convert the UUIDs for ( int i = 0; i < uuids.length; i++) { UUID u = new UUID(uuids[i]); + * if ( u.isValid()) { System.out.println("" + (i+1) + ": " + u.toString()); byte[] bytes = + * u.getBytes(); HexDump.Dump(bytes,bytes.length, 0); System.out.println("Convert to string: " + + * generateUUIDString(bytes)); } else System.out.println("Invalid UUID: " + uuids[i]); } } + */ +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/Wkssvc.java b/source/java/org/alfresco/filesys/smb/dcerpc/Wkssvc.java new file mode 100644 index 0000000000..2662887d70 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/Wkssvc.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc; + +/** + * Wkssvc Operation Ids Class + */ +public class Wkssvc +{ + // Wkssvc opcodes + + public static final int NetWkstaGetInfo = 0x00; + + /** + * Convert an opcode to a function name + * + * @param opCode int + * @return String + */ + public final static String getOpcodeName(int opCode) + { + String ret = ""; + switch (opCode) + { + case NetWkstaGetInfo: + ret = "NetWkstaGetInfo"; + break; + } + return ret; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfo.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfo.java new file mode 100644 index 0000000000..b2b4f359a0 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfo.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCEReadable; + +/** + * Connection Information Class + *

    + * Contains the details of a connection on a remote server. + */ +public class ConnectionInfo implements DCEReadable +{ + + // Information level + + private int m_infoLevel; + + // Connection id and type + + private int m_connId; + private int m_connType; + + // Count of open files + + private int m_openFiles; + + // Number of users + + private int m_numUsers; + + // Time connected, in minutes + + private int m_connTime; + + // User name + + private String m_userName; + + // Client name + + private String m_clientName; + + /** + * Default constructor + */ + public ConnectionInfo() + { + } + + /** + * Class constructor + * + * @param infoLevel int + */ + public ConnectionInfo(int infoLevel) + { + m_infoLevel = infoLevel; + } + + /** + * Get the information level + * + * @return int + */ + public final int getInformationLevel() + { + return m_infoLevel; + } + + /** + * Get the connection id + * + * @return int + */ + public final int getConnectionId() + { + return m_connId; + } + + /** + * Get the connection type + * + * @return int + */ + public final int getConnectionType() + { + return m_connType; + } + + /** + * Get the number of open files on the connection + * + * @return int + */ + public final int getOpenFileCount() + { + return m_openFiles; + } + + /** + * Return the number of users on the connection + * + * @return int + */ + public final int getNumberOfUsers() + { + return m_numUsers; + } + + /** + * Return the connection time in seconds + * + * @return int + */ + public final int getConnectionTime() + { + return m_connTime; + } + + /** + * Return the user name + * + * @return String + */ + public final String getUserName() + { + return m_userName; + } + + /** + * Return the client name + * + * @return String + */ + public final String getClientName() + { + return m_clientName; + } + + /** + * Read a connection information object from a DCE buffer + * + * @param buf DCEBuffer + * @throws DCEBufferException + */ + public void readObject(DCEBuffer buf) throws DCEBufferException + { + + // Unpack the connection information + + switch (getInformationLevel()) + { + + // Information level 0 + + case 0: + m_connId = buf.getInt(); + m_userName = null; + m_clientName = null; + break; + + // Information level 1 + + case 1: + m_connId = buf.getInt(); + m_connType = buf.getInt(); + m_openFiles = buf.getInt(); + m_numUsers = buf.getInt(); + m_connTime = buf.getInt(); + + m_userName = buf.getPointer() != 0 ? "" : null; + m_clientName = buf.getPointer() != 0 ? "" : null; + break; + } + } + + /** + * Read the strings for this connection information from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readStrings(DCEBuffer buf) throws DCEBufferException + { + + // Read the strings for this connection information + + switch (getInformationLevel()) + { + + // Information level 1 + + case 1: + if (getUserName() != null) + m_userName = buf.getString(DCEBuffer.ALIGN_INT); + if (getClientName() != null) + m_clientName = buf.getString(DCEBuffer.ALIGN_INT); + break; + } + } + + /** + * Return the connection information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("[ID="); + str.append(getConnectionId()); + str.append(":Level="); + str.append(getInformationLevel()); + str.append(":"); + + if (getInformationLevel() == 1) + { + str.append("Type="); + str.append(getConnectionType()); + str.append(",OpenFiles="); + str.append(getOpenFileCount()); + str.append(",NumUsers="); + str.append(getNumberOfUsers()); + str.append(",Connected="); + str.append(getConnectionTime()); + str.append(",User="); + str.append(getUserName()); + str.append(",Client="); + str.append(getClientName()); + } + + str.append("]"); + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfoList.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfoList.java new file mode 100644 index 0000000000..9eaed344b8 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/ConnectionInfoList.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCEList; +import org.alfresco.filesys.smb.dcerpc.DCEReadable; + +/** + * Connection Information List Class + */ +public class ConnectionInfoList extends DCEList +{ + + /** + * Default constructor + */ + public ConnectionInfoList() + { + super(); + } + + /** + * Class constructor + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public ConnectionInfoList(DCEBuffer buf) throws DCEBufferException + { + super(buf); + } + + /** + * Create a new connection information object + * + * @return DCEReadable + */ + protected DCEReadable getNewObject() + { + return new ConnectionInfo(getInformationLevel()); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/ServerInfo.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/ServerInfo.java new file mode 100644 index 0000000000..6b571f8280 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/ServerInfo.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCEReadable; +import org.alfresco.filesys.smb.dcerpc.DCEWriteable; + +/** + * Server Information Class + */ +public class ServerInfo implements DCEWriteable, DCEReadable +{ + + // Information levels supported + + public static final int InfoLevel0 = 0; + public static final int InfoLevel1 = 1; + public static final int InfoLevel101 = 101; + public static final int InfoLevel102 = 102; + + // Server platform ids + + public final static int PLATFORM_OS2 = 400; + public final static int PLATFORM_NT = 500; + + // Information level + + private int m_infoLevel; + + // Server information + + private int m_platformId; + private String m_name; + private int m_verMajor; + private int m_verMinor; + private int m_srvType; + private String m_comment; + + /** + * Default constructor + */ + public ServerInfo() + { + } + + /** + * Class constructor + * + * @param lev int + */ + public ServerInfo(int lev) + { + m_infoLevel = lev; + } + + /** + * Get the information level + * + * @return int + */ + public final int getInformationLevel() + { + return m_infoLevel; + } + + /** + * Get the server name + * + * @return String + */ + public final String getServerName() + { + return m_name; + } + + /** + * Get the server comment + * + * @return String + */ + public final String getComment() + { + return m_comment; + } + + /** + * Get the server platform id + * + * @return int + */ + public final int getPlatformId() + { + return m_platformId; + } + + /** + * Get the servev major version + * + * @return int + */ + public final int getMajorVersion() + { + return m_verMajor; + } + + /** + * Get the server minor version + * + * @return int + */ + public final int getMinorVersion() + { + return m_verMinor; + } + + /** + * Get the server type flags + * + * @return int + */ + public final int getServerType() + { + return m_srvType; + } + + /** + * Set the server name + * + * @param name String + */ + public final void setServerName(String name) + { + m_name = name; + } + + /** + * Set the server comment + * + * @param comment String + */ + public final void setComment(String comment) + { + m_comment = comment; + } + + /** + * Set the information level + * + * @param lev int + */ + public final void setInformationLevel(int lev) + { + m_infoLevel = lev; + } + + /** + * Set the server platform id + * + * @param id int + */ + public final void setPlatformId(int id) + { + m_platformId = id; + } + + /** + * Set the server type flags + * + * @param typ int + */ + public final void setServerType(int typ) + { + m_srvType = typ; + } + + /** + * Set the server version + * + * @param verMajor int + * @param verMinor int + */ + public final void setVersion(int verMajor, int verMinor) + { + m_verMajor = verMajor; + m_verMinor = verMinor; + } + + /** + * Clear the string values + */ + protected final void clearStrings() + { + + // Clear the string values + + m_name = null; + m_comment = null; + } + + /** + * Read the server information from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readObject(DCEBuffer buf) throws DCEBufferException + { + + // Clear the string values + + clearStrings(); + + // Read the server information details + + m_infoLevel = buf.getInt(); + buf.skipPointer(); + + // Unpack the server information + + switch (getInformationLevel()) + { + + // Information level 0 + + case InfoLevel0: + if (buf.getPointer() != 0) + m_name = buf.getString(DCEBuffer.ALIGN_INT); + break; + + // Information level 101/1 + + case InfoLevel1: + case InfoLevel101: + m_platformId = buf.getInt(); + buf.skipPointer(); + m_verMajor = buf.getInt(); + m_verMinor = buf.getInt(); + m_srvType = buf.getInt(); + buf.skipPointer(); + + m_name = buf.getString(DCEBuffer.ALIGN_INT); + m_comment = buf.getString(); + break; + + // Level 102 + + case InfoLevel102: + break; + } + } + + /** + * Read the strings for this object from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readStrings(DCEBuffer buf) throws DCEBufferException + { + + // Not required + } + + /** + * Write a server information structure + * + * @param buf DCEBuffer + * @param strBuf DCEBuffer + */ + public void writeObject(DCEBuffer buf, DCEBuffer strBuf) + { + + // Output the server information structure + + buf.putInt(getInformationLevel()); + buf.putPointer(true); + + // Output the required information level + + switch (getInformationLevel()) + { + + // Information level 0 + + case InfoLevel0: + buf.putPointer(getServerName() != null); + if (getServerName() != null) + strBuf.putString(getServerName(), DCEBuffer.ALIGN_INT, true); + break; + + // Information level 101/1 + + case InfoLevel1: + case InfoLevel101: + buf.putInt(getPlatformId()); + buf.putPointer(true); + buf.putInt(getMajorVersion()); + buf.putInt(getMinorVersion()); + buf.putInt(getServerType()); + buf.putPointer(true); + + strBuf.putString(getServerName(), DCEBuffer.ALIGN_INT, true); + strBuf.putString(getComment() != null ? getComment() : "", DCEBuffer.ALIGN_INT, true); + break; + + // Level 102 + + case InfoLevel102: + break; + } + } + + /** + * Return the server information as a string + * + * @return String + */ + public String toString() + { + return ""; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfo.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfo.java new file mode 100644 index 0000000000..cacad520e8 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfo.java @@ -0,0 +1,610 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCEReadable; +import org.alfresco.filesys.smb.dcerpc.DCEWriteable; + +/** + * Share Information Class + *

    + * Holds the details of a share from a DCE/RPC request/response. + */ +public class ShareInfo implements DCEWriteable, DCEReadable +{ + + // Information levels supported + + public static final int InfoLevel0 = 0; + public static final int InfoLevel1 = 1; + public static final int InfoLevel2 = 2; + public static final int InfoLevel502 = 502; + public static final int InfoLevel1005 = 1005; + + // Share types + + public static final int Disk = 0x00000000; + public static final int PrintQueue = 0x00000001; + public static final int Device = 0x00000002; + public static final int IPC = 0x00000003; + public static final int Hidden = 0x80000000; + + // Share permission flags + + public static final int Read = 0x01; + public static final int Write = 0x02; + public static final int Create = 0x04; + public static final int Execute = 0x08; + public static final int Delete = 0x10; + public static final int Attrib = 0x20; + public static final int Perm = 0x40; + public static final int All = 0x7F; + + // Information level + + private int m_infoLevel; + + // Share details + + private String m_name; + private int m_type; + private String m_comment; + + private int m_permissions; + private int m_maxUsers; + private int m_curUsers; + private String m_path; + private String m_password; + + private int m_flags; + + /** + * Class constructor + */ + public ShareInfo() + { + } + + /** + * Class constructor + * + * @param lev int + */ + public ShareInfo(int lev) + { + m_infoLevel = lev; + } + + /** + * Class constructor + * + * @param lev int + * @param name String + * @param typ int + * @param comment String + */ + public ShareInfo(int lev, String name, int typ, String comment) + { + m_infoLevel = lev; + m_name = name; + m_type = typ; + m_comment = comment; + } + + /** + * Return the information level + * + * @return int + */ + public final int getInformationLevel() + { + return m_infoLevel; + } + + /** + * Return the share name + * + * @return String + */ + public final String getName() + { + return m_name; + } + + /** + * Return the share type + * + * @return int + */ + public final int getType() + { + return m_type; + } + + /** + * Get the share flags + * + * @return int + */ + public final int getFlags() + { + return m_flags; + } + + /** + * Check if this share is a hidden/admin share + * + * @return boolean + */ + public final boolean isHidden() + { + return (m_type & Hidden) != 0 ? true : false; + } + + /** + * Check if this is a disk share + * + * @return boolean + */ + public final boolean isDisk() + { + return (m_type & 0x0000FFFF) == Disk ? true : false; + } + + /** + * Check if this is a printer share + * + * @return boolean + */ + public final boolean isPrinter() + { + return (m_type & 0x0000FFFF) == PrintQueue ? true : false; + } + + /** + * Check if this is a device share + * + * @return boolean + */ + public final boolean isDevice() + { + return (m_type & 0x0000FFFF) == Device ? true : false; + } + + /** + * Check if this is a named pipe share + * + * @return boolean + */ + public final boolean isNamedPipe() + { + return (m_type & 0x0000FFFF) == IPC ? true : false; + } + + /** + * Return the share permissions + * + * @return int + */ + public final int getPermissions() + { + return m_permissions; + } + + /** + * Return the maximum number of users allowed + * + * @return int + */ + public final int getMaximumUsers() + { + return m_maxUsers; + } + + /** + * Return the current number of users + * + * @return int + */ + public final int getCurrentUsers() + { + return m_curUsers; + } + + /** + * Return the share local path + * + * @return String + */ + public final String getPath() + { + return m_path; + } + + /** + * Return the share password + * + * @return String + */ + public final String getPassword() + { + return m_password; + } + + /** + * Return the share type as a string + * + * @return String + */ + public final String getTypeAsString() + { + + String typ = ""; + switch (getType() & 0xFF) + { + case Disk: + typ = "Disk"; + break; + case PrintQueue: + typ = "Printer"; + break; + case Device: + typ = "Device"; + break; + case IPC: + typ = "IPC"; + break; + } + + return typ; + } + + /** + * Return the comment + * + * @return String + */ + public final String getComment() + { + return m_comment; + } + + /** + * Set the information level + * + * @param lev int + */ + public final void setInformationLevel(int lev) + { + m_infoLevel = lev; + } + + /** + * Set the share type + * + * @param int typ + */ + public final void setType(int typ) + { + m_type = typ; + } + + /** + * Set the share flags + * + * @param flags int + */ + public final void setFlags(int flags) + { + m_flags = flags; + } + + /** + * Set the share name + * + * @param name String + */ + public final void setName(String name) + { + m_name = name; + } + + /** + * Set the share comment + * + * @param str String + */ + public final void setComment(String str) + { + m_comment = str; + } + + /** + * Set the share permissions + * + * @param perm int + */ + public final void setPermissions(int perm) + { + m_permissions = perm; + } + + /** + * Set the maximum number of users + * + * @param maxUsers int + */ + public final void setMaximumUsers(int maxUsers) + { + m_maxUsers = maxUsers; + } + + /** + * Set the current number of users + * + * @param curUsers int + */ + public final void setCurrentUsers(int curUsers) + { + m_curUsers = curUsers; + } + + /** + * Set the local path + * + * @param path String + */ + public final void setPath(String path) + { + m_path = path; + } + + /** + * Clear all string values + */ + protected final void clearStrings() + { + + // Clear the string values + + m_name = null; + m_comment = null; + m_path = null; + m_password = null; + } + + /** + * Read the share information from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readObject(DCEBuffer buf) throws DCEBufferException + { + + // Clear all existing strings + + clearStrings(); + + // Unpack the share information + + switch (getInformationLevel()) + { + + // Information level 0 + + case InfoLevel0: + m_name = buf.getPointer() != 0 ? "" : null; + break; + + // Information level 1 + + case InfoLevel1: + m_name = buf.getPointer() != 0 ? "" : null; + m_type = buf.getInt(); + m_comment = buf.getPointer() != 0 ? "" : null; + break; + + // Information level 2 + + case InfoLevel2: + m_name = buf.getPointer() != 0 ? "" : null; + m_type = buf.getInt(); + m_comment = buf.getPointer() != 0 ? "" : null; + m_permissions = buf.getInt(); + m_maxUsers = buf.getInt(); + m_curUsers = buf.getInt(); + m_path = buf.getPointer() != 0 ? "" : null; + m_password = buf.getPointer() != 0 ? "" : null; + break; + + // Information level 502 + + case InfoLevel502: + m_name = buf.getPointer() != 0 ? "" : null; + m_type = buf.getInt(); + m_comment = buf.getPointer() != 0 ? "" : null; + m_permissions = buf.getInt(); + m_maxUsers = buf.getInt(); + m_curUsers = buf.getInt(); + m_path = buf.getPointer() != 0 ? "" : null; + m_password = buf.getPointer() != 0 ? "" : null; + + buf.skipBytes(4); // Reserved value + + // Security descriptor + break; + } + } + + /** + * Read the strings for this share from the DCE/RPC buffer + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public void readStrings(DCEBuffer buf) throws DCEBufferException + { + + // Read the strings for this share information + + switch (getInformationLevel()) + { + + // Information level 0 + + case InfoLevel0: + if (getName() != null) + m_name = buf.getString(DCEBuffer.ALIGN_INT); + break; + + // Information level 1 + + case InfoLevel1: + if (getName() != null) + m_name = buf.getString(DCEBuffer.ALIGN_INT); + if (getComment() != null) + m_comment = buf.getString(DCEBuffer.ALIGN_INT); + break; + + // Information level 2 and 502 + + case InfoLevel2: + case InfoLevel502: + if (getName() != null) + m_name = buf.getString(DCEBuffer.ALIGN_INT); + if (getComment() != null) + m_comment = buf.getString(DCEBuffer.ALIGN_INT); + if (getPath() != null) + m_path = buf.getString(DCEBuffer.ALIGN_INT); + if (getPassword() != null) + m_password = buf.getString(DCEBuffer.ALIGN_INT); + break; + } + } + + /** + * Write the share information to the DCE buffer + * + * @param buf DCEBuffer + * @param strBuf DCEBuffer + */ + public void writeObject(DCEBuffer buf, DCEBuffer strBuf) + { + + // Pack the share information + + switch (getInformationLevel()) + { + + // Information level 0 + + case InfoLevel0: + buf.putPointer(true); + strBuf.putString(getName(), DCEBuffer.ALIGN_INT, true); + break; + + // Information level 1 + + case InfoLevel1: + buf.putPointer(true); + buf.putInt(getType()); + buf.putPointer(true); + + strBuf.putString(getName(), DCEBuffer.ALIGN_INT, true); + strBuf.putString(getComment() != null ? getComment() : "", DCEBuffer.ALIGN_INT, true); + break; + + // Information level 2 + + case InfoLevel2: + buf.putPointer(true); + buf.putInt(getType()); + buf.putPointer(true); + buf.putInt(getPermissions()); + buf.putInt(getMaximumUsers()); + buf.putInt(getCurrentUsers()); + buf.putPointer(getPath() != null); + buf.putPointer(getPassword() != null); + + strBuf.putString(getName(), DCEBuffer.ALIGN_INT, true); + strBuf.putString(getComment() != null ? getComment() : "", DCEBuffer.ALIGN_INT, true); + if (getPath() != null) + strBuf.putString(getPath(), DCEBuffer.ALIGN_INT, true); + if (getPassword() != null) + strBuf.putString(getPassword(), DCEBuffer.ALIGN_INT, true); + break; + + // Information level 502 + + case InfoLevel502: + buf.putPointer(true); + buf.putInt(getType()); + buf.putPointer(true); + buf.putInt(getPermissions()); + buf.putInt(getMaximumUsers()); + buf.putInt(getCurrentUsers()); + buf.putPointer(getPath() != null); + buf.putPointer(getPassword() != null); + buf.putInt(0); // Reserved, must be zero + buf.putPointer(false); // Security descriptor + + strBuf.putString(getName(), DCEBuffer.ALIGN_INT, true); + strBuf.putString(getComment() != null ? getComment() : "", DCEBuffer.ALIGN_INT, true); + if (getPath() != null) + strBuf.putString(getPath(), DCEBuffer.ALIGN_INT, true); + if (getPassword() != null) + strBuf.putString(getPassword(), DCEBuffer.ALIGN_INT, true); + break; + + // Information level 1005 + + case InfoLevel1005: + buf.putInt(getFlags()); + break; + } + } + + /** + * Return the share information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getName()); + str.append(":"); + str.append(getInformationLevel()); + str.append(":"); + + if (getInformationLevel() == 1) + { + str.append("0x"); + str.append(Integer.toHexString(getType())); + str.append(","); + str.append(getComment()); + } + + str.append("]"); + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfoList.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfoList.java new file mode 100644 index 0000000000..a8b4b2584d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/ShareInfoList.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import java.util.Vector; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCEList; +import org.alfresco.filesys.smb.dcerpc.DCEReadable; + +/** + * Server Share Information List Class + *

    + * Holds the details for a DCE/RPC share enumeration request or response. + */ +public class ShareInfoList extends DCEList +{ + + /** + * Default constructor + */ + public ShareInfoList() + { + super(); + } + + /** + * Class constructor + * + * @param buf DCEBuffer + * @exception DCEBufferException + */ + public ShareInfoList(DCEBuffer buf) throws DCEBufferException + { + super(buf); + } + + /** + * Class constructor + * + * @param infoLevel int + */ + public ShareInfoList(int infoLevel) + { + super(infoLevel); + } + + /** + * Return share information object from the list + * + * @param idx int + * @return ShareInfo + */ + public final ShareInfo getShare(int idx) + { + return (ShareInfo) getElement(idx); + } + + /** + * Create a new share information object + * + * @return DCEReadable + */ + protected DCEReadable getNewObject() + { + return new ShareInfo(getInformationLevel()); + } + + /** + * Add a share to the list + * + * @param share ShareInfo + */ + public final void addShare(ShareInfo share) + { + + // Check if the share list is valid + + if (getList() == null) + setList(new Vector()); + + // Add the share + + getList().add(share); + } + + /** + * Set the share information list + * + * @param list Vector + */ + public final void setShareList(Vector list) + { + setList(list); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/UserInfo.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/UserInfo.java new file mode 100644 index 0000000000..c972b0b732 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/UserInfo.java @@ -0,0 +1,773 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import java.util.BitSet; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCEReadable; + +/** + * User Information Class + *

    + * Contains the details of a user account on a remote server. + */ +public class UserInfo implements DCEReadable +{ + + // Information levels supported + + public static final int InfoLevel1 = 1; + public static final int InfoLevel3 = 3; + public static final int InfoLevel21 = 21; + + // public static final int InfoLevel2 = 2; + // public static final int InfoLevel4 = 4; + // public static final int InfoLevel5 = 5; + // public static final int InfoLevel6 = 6; + // public static final int InfoLevel7 = 7; + // public static final int InfoLevel8 = 8; + // public static final int InfoLevel9 = 9; + // public static final int InfoLevel10 = 10; + // public static final int InfoLevel11 = 11; + // public static final int InfoLevel12 = 12; + // public static final int InfoLevel13 = 13; + // public static final int InfoLevel14 = 14; + // public static final int InfoLevel16 = 16; + // public static final int InfoLevel17 = 17; + // public static final int InfoLevel20 = 20; + + // Account privilege levels + + public static final int PrivGuest = 0; + public static final int PrivUser = 1; + public static final int PrivAdmin = 2; + + // Account operator privileges + + public static final int OperPrint = 0; + public static final int OperComm = 1; + public static final int OperServer = 2; + public static final int OperAccounts = 3; + + // Account flags + + private static final int AccountDisabled = 0x0001; + private static final int AccountHomeDirRequired = 0x0002; + private static final int AccountPasswordNotRequired = 0x0004; + private static final int AccountTemporaryDuplicate = 0x0008; + private static final int AccountNormal = 0x0010; + private static final int AccountMNSUser = 0x0020; + private static final int AccountDomainTrust = 0x0040; + private static final int AccountWorkstationTrust = 0x0080; + private static final int AccountServerTrust = 0x0100; + private static final int AccountPasswordNotExpire = 0x0200; + private static final int AccountAutoLocked = 0x0400; + + // Information level + + private int m_infoLevel; + + // User information + + private String m_userName; + + private int m_pwdAge; + private int m_priv; + + private String m_homeDir; + private String m_comment; + private String m_description; + private String m_accComment; + + private int m_flags; + + private String m_scriptPath; + // private int m_authFlags; + + private String m_fullName; + private String m_appParam; + private String m_workStations; + + private long m_lastLogon; + private long m_lastLogoff; + private long m_acctExpires; + private long m_lastPwdChange; + private long m_pwdCanChange; + private long m_pwdMustchange; + + // private int m_maxStorage; + private int m_unitsPerWeek; + private byte[] m_logonHoursRaw; + private BitSet m_logonHours; + + private int m_badPwdCount; + private int m_numLogons; + private String logonSrv; + + private int m_countryCode; + private int m_codePage; + + private int m_userRID; + private int m_groupRID; + // private SID m_userSID; + + private String m_profile; + private String m_homeDirDrive; + + private int m_pwdExpired; + + private String m_callBack; + private String m_unknown1; + private String m_unknown2; + private String m_unknown3; + + /** + * Default constructor + */ + public UserInfo() + { + } + + /** + * Class constructor + * + * @param lev int + */ + public UserInfo(int lev) + { + m_infoLevel = lev; + } + + /** + * Get the information level + * + * @return int + */ + public final int getInformationLevel() + { + return m_infoLevel; + } + + /** + * Return the logon server name + * + * @return String + */ + public final String getLogonServer() + { + return logonSrv; + } + + /** + * Return the date/time the account expires, or NTTime.Infinity if it does not expire + * + * @return long + */ + public final long getAccountExpires() + { + return m_acctExpires; + } + + /** + * Return the application parameter string + * + * @return String + */ + public final String getApplicationParameter() + { + return m_appParam; + } + + /** + * Return the bad password count + * + * @return int + */ + public final int getBadPasswordCount() + { + return m_badPwdCount; + } + + /** + * Return the code page + * + * @return int + */ + public final int getCodePage() + { + return m_codePage; + } + + /** + * Return the account comment + * + * @return String + */ + public final String getComment() + { + return m_comment; + } + + /** + * Return the account description + * + * @return String + */ + public final String getDescription() + { + return m_description; + } + + /** + * Return the country code + * + * @return int + */ + public final int getCountryCode() + { + return m_countryCode; + } + + /** + * Return the account flags + * + * @return int + */ + public final int getFlags() + { + return m_flags; + } + + /** + * Check if the account is disabled + * + * @return boolean + */ + public final boolean isDisabled() + { + return (m_flags & AccountDisabled) != 0 ? true : false; + } + + /** + * Check if the account does not require a home directory + * + * @return boolean + */ + public final boolean requiresHomeDirectory() + { + return (m_flags & AccountHomeDirRequired) != 0 ? true : false; + } + + /** + * Check if the account does not require a password + * + * @return boolean + */ + public final boolean requiresPassword() + { + return (m_flags & AccountPasswordNotRequired) != 0 ? false : true; + } + + /** + * Check if the account is a normal user account + * + * @return boolean + */ + public final boolean isNormalUser() + { + return (m_flags & AccountNormal) != 0 ? true : false; + } + + /** + * Check if the account is a domain trust account + * + * @return boolean + */ + public final boolean isDomainTrust() + { + return (m_flags & AccountDomainTrust) != 0 ? true : false; + } + + /** + * Check if the account is a workstation trust account + * + * @return boolean + */ + public final boolean isWorkstationTrust() + { + return (m_flags & AccountWorkstationTrust) != 0 ? true : false; + } + + /** + * Check if the account is a server trust account + * + * @return boolean + */ + public final boolean isServerTrust() + { + return (m_flags & AccountServerTrust) != 0 ? true : false; + } + + /** + * Check if the account password expires + * + * @return boolean + */ + public final boolean passwordExpires() + { + return (m_flags & AccountPasswordNotExpire) != 0 ? false : true; + } + + /** + * Check if the account is auto locked + * + * @return boolean + */ + public final boolean isAutoLocked() + { + return (m_flags & AccountAutoLocked) != 0 ? true : false; + } + + /** + * Return the full account name + * + * @return String + */ + public final String getFullName() + { + return m_fullName; + } + + /** + * Return the group resource id + * + * @return int + */ + public final int getGroupRID() + { + return m_groupRID; + } + + /** + * Return the home directory path + * + * @return String + */ + public final String getHomeDirectory() + { + return m_homeDir; + } + + /** + * Return the home drive + * + * @return String + */ + public final String getHomeDirectoryDrive() + { + return m_homeDirDrive; + } + + /** + * Return the date/time of last logoff + * + * @return long + */ + public final long getLastLogoff() + { + return m_lastLogoff; + } + + /** + * Return the date/time of last logon, to this server + * + * @return long + */ + public final long getLastLogon() + { + return m_lastLogon; + } + + /** + * Return the allowed logon hours bit set + * + * @return BitSet + */ + public final BitSet getLogonHours() + { + return m_logonHours; + } + + /** + * Return the number of logons for the account, to this server + * + * @return int + */ + public final int numberOfLogons() + { + return m_numLogons; + } + + /** + * Return the account provileges + * + * @return int + */ + public final int getPrivileges() + { + return m_priv; + } + + /** + * Return the profile path + * + * @return String + */ + public final String getProfile() + { + return m_profile; + } + + /** + * Return the password expired flag + * + * @return int + */ + public final int getPasswordExpired() + { + return m_pwdExpired; + } + + /** + * Return the logon script path + * + * @return String + */ + public final String getLogonScriptPath() + { + return m_scriptPath; + } + + /** + * Return the allowed units per week + * + * @return int + */ + public final int getUnitsPerWeek() + { + return m_unitsPerWeek; + } + + /** + * Return the account name + * + * @return String + */ + public final String getUserName() + { + return m_userName; + } + + /** + * Return the user resource id + * + * @return int + */ + public final int getUserRID() + { + return m_userRID; + } + + /** + * Return the workstations that the account is allowed to logon from + * + * @return String + */ + public final String getWorkStations() + { + return m_workStations; + } + + /** + * Return the date/time of the last password change + * + * @return long + */ + public final long getLastPasswordChange() + { + return m_lastPwdChange; + } + + /** + * Return the date/time that the password must be changed by + * + * @return long + */ + public final long getPasswordMustChangeBy() + { + return m_pwdMustchange; + } + + /** + * Clear all string values + */ + private final void clearStrings() + { + + // Clear the string values + + m_appParam = null; + m_comment = null; + m_fullName = null; + m_homeDir = null; + m_homeDirDrive = null; + m_profile = null; + m_scriptPath = null; + m_userName = null; + m_workStations = null; + m_description = null; + m_accComment = null; + } + + /** + * Read the user information from the DCE buffer + * + * @param buf DCEBuffer + * @throws DCEBufferException + */ + public void readObject(DCEBuffer buf) throws DCEBufferException + { + + // clear all existing string values + + clearStrings(); + + // Unpack the user information + + int ival = 0; + int pval = 0; + + switch (getInformationLevel()) + { + + // Information level 1 + + case InfoLevel1: + m_userName = buf.getCharArrayPointer(); + m_fullName = buf.getCharArrayPointer(); + m_groupRID = buf.getInt(); + m_description = buf.getCharArrayPointer(); + m_comment = buf.getCharArrayPointer(); + break; + + // Information level 3 + + case InfoLevel3: + m_userName = buf.getCharArrayPointer(); + m_fullName = buf.getCharArrayPointer(); + + m_userRID = buf.getInt(); + m_groupRID = buf.getInt(); + + m_homeDir = buf.getCharArrayPointer(); + m_homeDirDrive = buf.getCharArrayPointer(); + m_scriptPath = buf.getCharArrayPointer(); + m_profile = buf.getCharArrayPointer(); + m_workStations = buf.getCharArrayPointer(); + + m_lastLogon = buf.getNTTime(); + m_lastLogoff = buf.getNTTime(); + m_lastPwdChange = buf.getNTTime(); + buf.skipBytes(8); // allow password change NT time + buf.skipBytes(8); // force password change NT time + + ival = buf.getShort(DCEBuffer.ALIGN_INT); + pval = buf.getPointer(); + + if (ival != 0 && pval != 0) + m_logonHoursRaw = new byte[ival / 8]; + + m_badPwdCount = buf.getShort(); + m_numLogons = buf.getShort(); + + m_flags = buf.getInt(); + break; + + // Information level 21 + + case InfoLevel21: + m_lastLogon = buf.getNTTime(); + m_lastLogoff = buf.getNTTime(); + m_lastPwdChange = buf.getNTTime(); + m_acctExpires = buf.getNTTime(); + m_pwdCanChange = buf.getNTTime(); + m_pwdMustchange = buf.getNTTime(); + + m_userName = buf.getCharArrayPointer(); + m_fullName = buf.getCharArrayPointer(); + + m_homeDir = buf.getCharArrayPointer(); + m_homeDirDrive = buf.getCharArrayPointer(); + m_scriptPath = buf.getCharArrayPointer(); + m_profile = buf.getCharArrayPointer(); + m_description = buf.getCharArrayPointer(); + m_workStations = buf.getCharArrayPointer(); + m_accComment = buf.getCharArrayPointer(); + + m_callBack = buf.getCharArrayPointer(); + m_unknown1 = buf.getCharArrayPointer(); + m_unknown2 = buf.getCharArrayPointer(); + m_unknown3 = buf.getCharArrayPointer(); + + buf.skipBytes(8); // buffer length and pointer + + m_userRID = buf.getInt(); + m_groupRID = buf.getInt(); + + m_flags = buf.getInt(); + + buf.getInt(); // fields present flags + + ival = buf.getShort(DCEBuffer.ALIGN_INT); + pval = buf.getPointer(); + + if (ival != 0 && pval != 0) + m_logonHoursRaw = new byte[ival / 8]; + + m_badPwdCount = buf.getShort(); + m_numLogons = buf.getShort(); + + m_countryCode = buf.getShort(); + m_codePage = buf.getShort(); + + buf.skipBytes(2); // NT and LM pwd set flags + + m_pwdExpired = buf.getByte(DCEBuffer.ALIGN_INT); + break; + } + } + + /** + * Read the strings for this user information from the DCE buffer + * + * @param buf DCEBuffer + * @throws DCEBufferException + */ + public void readStrings(DCEBuffer buf) throws DCEBufferException + { + + // Read the strings/structures for this user information + + switch (getInformationLevel()) + { + + // Information level 1 + + case InfoLevel1: + m_userName = buf.getCharArrayNotNull(m_userName, DCEBuffer.ALIGN_INT); + m_fullName = buf.getCharArrayNotNull(m_fullName, DCEBuffer.ALIGN_INT); + + m_description = buf.getCharArrayNotNull(m_description, DCEBuffer.ALIGN_INT); + m_comment = buf.getCharArrayNotNull(m_comment, DCEBuffer.ALIGN_INT); + break; + + // Information level 3 + + case InfoLevel3: + m_userName = buf.getCharArrayNotNull(m_userName, DCEBuffer.ALIGN_INT); + m_fullName = buf.getCharArrayNotNull(m_fullName, DCEBuffer.ALIGN_INT); + + m_homeDir = buf.getCharArrayNotNull(m_homeDir, DCEBuffer.ALIGN_INT); + m_homeDirDrive = buf.getCharArrayNotNull(m_homeDirDrive, DCEBuffer.ALIGN_INT); + + m_scriptPath = buf.getCharArrayNotNull(m_scriptPath, DCEBuffer.ALIGN_INT); + m_profile = buf.getCharArrayNotNull(m_profile, DCEBuffer.ALIGN_INT); + m_workStations = buf.getCharArrayNotNull(m_workStations, DCEBuffer.ALIGN_INT); + + m_logonHoursRaw = buf.getByteStructure(m_logonHoursRaw); + break; + + // Information level 21 + + case InfoLevel21: + m_userName = buf.getCharArrayNotNull(m_userName, DCEBuffer.ALIGN_INT); + m_fullName = buf.getCharArrayNotNull(m_fullName, DCEBuffer.ALIGN_INT); + + m_homeDir = buf.getCharArrayNotNull(m_homeDir, DCEBuffer.ALIGN_INT); + m_homeDirDrive = buf.getCharArrayNotNull(m_homeDirDrive, DCEBuffer.ALIGN_INT); + + m_scriptPath = buf.getCharArrayNotNull(m_scriptPath, DCEBuffer.ALIGN_INT); + m_profile = buf.getCharArrayNotNull(m_profile, DCEBuffer.ALIGN_INT); + m_description = buf.getCharArrayNotNull(m_description, DCEBuffer.ALIGN_INT); + m_workStations = buf.getCharArrayNotNull(m_workStations, DCEBuffer.ALIGN_INT); + m_accComment = buf.getCharArrayNotNull(m_profile, DCEBuffer.ALIGN_INT); + + m_callBack = buf.getCharArrayNotNull(m_callBack, DCEBuffer.ALIGN_INT); + m_unknown1 = buf.getCharArrayNotNull(m_unknown1, DCEBuffer.ALIGN_INT); + m_unknown2 = buf.getCharArrayNotNull(m_unknown2, DCEBuffer.ALIGN_INT); + m_unknown3 = buf.getCharArrayNotNull(m_unknown3, DCEBuffer.ALIGN_INT); + + m_logonHoursRaw = buf.getByteStructure(m_logonHoursRaw); + break; + } + } + + /** + * Return an account type as a string + * + * @param typ int + * @return String + */ + public final static String getAccountTypeAsString(int typ) + { + String ret = ""; + switch (typ) + { + case PrivGuest: + ret = "Guest"; + break; + case PrivUser: + ret = "User"; + break; + case PrivAdmin: + ret = "Administrator"; + break; + } + return ret; + } + + /** + * Return the user information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getUserName()); + str.append(":"); + str.append(getInformationLevel()); + str.append(":"); + + str.append("]"); + return str.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/info/WorkstationInfo.java b/source/java/org/alfresco/filesys/smb/dcerpc/info/WorkstationInfo.java new file mode 100644 index 0000000000..c49acd0bc9 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/info/WorkstationInfo.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.info; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEWriteable; + +/** + * Workstation Information Class + */ +public class WorkstationInfo implements DCEWriteable +{ + + // Supported information levels + + public static final int InfoLevel100 = 100; + + // Information level + + private int m_infoLevel; + + // Server information + + private int m_platformId; + private String m_name; + private String m_domain; + private int m_verMajor; + private int m_verMinor; + + private String m_userName; + private String m_logonDomain; + private String m_otherDomain; + + /** + * Default constructor + */ + public WorkstationInfo() + { + } + + /** + * Class constructor + * + * @param lev int + */ + public WorkstationInfo(int lev) + { + m_infoLevel = lev; + } + + /** + * Get the information level + * + * @return int + */ + public final int getInformationLevel() + { + return m_infoLevel; + } + + /** + * Get the workstation name + * + * @return String + */ + public final String getWorkstationName() + { + return m_name; + } + + /** + * Get the domain/workgroup + * + * @return String + */ + public final String getDomain() + { + return m_domain; + } + + /** + * Get the workstation platform id + * + * @return int + */ + public final int getPlatformId() + { + return m_platformId; + } + + /** + * Get the workstation major version + * + * @return int + */ + public final int getMajorVersion() + { + return m_verMajor; + } + + /** + * Get the workstation minor version + * + * @return int + */ + public final int getMinorVersion() + { + return m_verMinor; + } + + /** + * Reutrn the user name + * + * @return String + */ + public final String getUserName() + { + return m_userName; + } + + /** + * Return the workstations logon domain. + * + * @return java.lang.String + */ + public String getLogonDomain() + { + return m_logonDomain; + } + + /** + * Return the list of domains that the workstation is enlisted in. + * + * @return java.lang.String + */ + public String getOtherDomains() + { + return m_otherDomain; + } + + /** + * Set the logon domain name. + * + * @param logdom java.lang.String + */ + public void setLogonDomain(String logdom) + { + m_logonDomain = logdom; + } + + /** + * Set the other domains that this workstation is enlisted in. + * + * @param othdom java.lang.String + */ + public void setOtherDomains(String othdom) + { + m_otherDomain = othdom; + } + + /** + * Set the workstation name + * + * @param name String + */ + public final void setWorkstationName(String name) + { + m_name = name; + } + + /** + * Set the domain/workgroup + * + * @param domain String + */ + public final void setDomain(String domain) + { + m_domain = domain; + } + + /** + * Set the information level + * + * @param lev int + */ + public final void setInformationLevel(int lev) + { + m_infoLevel = lev; + } + + /** + * Set the platform id + * + * @param id int + */ + public final void setPlatformId(int id) + { + m_platformId = id; + } + + /** + * Set the version + * + * @param verMajor int + * @param verMinor int + */ + public final void setVersion(int verMajor, int verMinor) + { + m_verMajor = verMajor; + m_verMinor = verMinor; + } + + /** + * Set the logged in user name + * + * @param user String + */ + public final void setUserName(String user) + { + m_userName = user; + } + + /** + * Clear the string values + */ + protected final void clearStrings() + { + + // Clear the string values + + m_userName = null; + m_domain = null; + m_logonDomain = null; + m_otherDomain = null; + } + + /** + * Write a workstation information structure + * + * @param buf DCEBuffer + * @param strBuf DCEBuffer + */ + public void writeObject(DCEBuffer buf, DCEBuffer strBuf) + { + + // Output the workstation information structure + + buf.putInt(getInformationLevel()); + buf.putPointer(true); + + // Output the required information level + + switch (getInformationLevel()) + { + + // Level 100 + + case InfoLevel100: + buf.putInt(getPlatformId()); + buf.putPointer(true); + buf.putPointer(true); + buf.putInt(getMajorVersion()); + buf.putInt(getMinorVersion()); + + strBuf.putString(getWorkstationName(), DCEBuffer.ALIGN_INT, true); + strBuf.putString(getDomain() != null ? getDomain() : "", DCEBuffer.ALIGN_INT, true); + break; + + // Level 101 + + case 101: + break; + + // Level 102 + + case 102: + break; + } + } + + /** + * Return the workstation information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + + str.append(getWorkstationName()); + str.append(":Domain="); + str.append(getDomain()); + str.append(":User="); + str.append(getUserName()); + str.append(":Id="); + str.append(getPlatformId()); + + str.append(":v"); + str.append(getMajorVersion()); + str.append("."); + str.append(getMinorVersion()); + + // Optional strings + + if (getLogonDomain() != null) + { + str.append(":Logon="); + str.append(getLogonDomain()); + } + + if (getOtherDomains() != null) + { + str.append(":Other="); + str.append(getOtherDomains()); + } + + // Return the workstation information as a string + + str.append("]"); + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEHandler.java b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEHandler.java new file mode 100644 index 0000000000..e939628c29 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.server; + +import java.io.IOException; + +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.server.SMBSrvException; +import org.alfresco.filesys.smb.server.SMBSrvSession; + +/** + * DCE Request Handler Interface + */ +public interface DCEHandler +{ + + /** + * Process a DCE/RPC request + * + * @param sess SMBSrvSession + * @param inBuf DCEBuffer + * @param pipeFile DCEPipeFile + * @exception IOException + * @exception SMBSrvException + */ + public void processRequest(SMBSrvSession sess, DCEBuffer inBuf, DCEPipeFile pipeFile) throws IOException, + SMBSrvException; +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeFile.java b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeFile.java new file mode 100644 index 0000000000..200f8c0ce9 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeFile.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.server; + +import java.io.IOException; + +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEPipeType; + +/** + * DCE/RPC Pipe File Class + *

    + * Contains the details and state of a DCE/RPC special named pipe. + */ +public class DCEPipeFile extends NetworkFile +{ + + // Maximum receive/transmit DCE fragment size + + private int m_maxRxFragSize; + private int m_maxTxFragSize; + + // Named pipe state flags + + private int m_state; + + // DCE/RPC handler for this named pipe + + private DCEHandler m_handler; + + // Current DCE buffered data + + private DCEBuffer m_dceData; + + /** + * Class constructor + * + * @param id int + */ + public DCEPipeFile(int id) + { + super(id); + setName(DCEPipeType.getTypeAsString(id)); + + // Set the DCE/RPC request handler for the pipe + + setRequestHandler(DCEPipeHandler.getHandlerForType(id)); + } + + /** + * Return the maximum receive fragment size + * + * @return int + */ + public final int getMaxReceiveFragmentSize() + { + return m_maxRxFragSize; + } + + /** + * Return the maximum transmit fragment size + * + * @return int + */ + public final int getMaxTransmitFragmentSize() + { + return m_maxTxFragSize; + } + + /** + * Return the named pipe state + * + * @return int + */ + public final int getPipeState() + { + return m_state; + } + + /** + * Return the pipe type id + * + * @return int + */ + public final int getPipeId() + { + return getFileId(); + } + + /** + * Determine if the pipe has a request handler + * + * @return boolean + */ + public final boolean hasRequestHandler() + { + return m_handler != null ? true : false; + } + + /** + * Return the pipes DCE/RPC handler + * + * @return DCEHandler + */ + public final DCEHandler getRequestHandler() + { + return m_handler; + } + + /** + * Determine if the pipe has any buffered data + * + * @return boolean + */ + public final boolean hasBufferedData() + { + return m_dceData != null ? true : false; + } + + /** + * Get the buffered data for the pipe + * + * @return DCEBuffer + */ + public final DCEBuffer getBufferedData() + { + return m_dceData; + } + + /** + * Set buffered data for the pipe + * + * @param buf DCEBuffer + */ + public final void setBufferedData(DCEBuffer buf) + { + m_dceData = buf; + } + + /** + * Set the maximum receive fragment size + * + * @param siz int + */ + public final void setMaxReceiveFragmentSize(int siz) + { + m_maxRxFragSize = siz; + } + + /** + * Set the maximum transmit fragment size + * + * @param siz int + */ + public final void setMaxTransmitFragmentSize(int siz) + { + m_maxTxFragSize = siz; + } + + /** + * Set the named pipe state flags + * + * @param state int + */ + public final void setPipeState(int state) + { + m_state = state; + } + + /** + * Set the pipes DCE/RPC handler + * + * @param handler DCEHandler + */ + public final void setRequestHandler(DCEHandler handler) + { + m_handler = handler; + } + + /** + * Dump the file details + */ + public final void DumpFile() + { + System.out.println("** DCE/RPC Named Pipe: " + getName()); + System.out.println(" File ID : " + getFileId()); + System.out.println(" State : 0x" + Integer.toHexString(getPipeState())); + System.out.println(" Max Rx : " + getMaxReceiveFragmentSize()); + System.out.println(" Max Tx : " + getMaxTransmitFragmentSize()); + System.out.println(" Handler : " + getRequestHandler()); + } + + /** + * @see NetworkFile#closeFile() + */ + public void closeFile() throws IOException + { + } + + /** + * @see NetworkFile#openFile(boolean) + */ + public void openFile(boolean createFlag) throws IOException + { + } + + /** + * @see NetworkFile#readFile(byte[], int, int, long) + */ + public int readFile(byte[] buf, int len, int pos, long fileOff) throws IOException + { + return 0; + } + + /** + * Flush any buffered output to the file + * + * @throws IOException + */ + public void flushFile() throws IOException + { + } + + /** + * @see NetworkFile#seekFile(long, int) + */ + public long seekFile(long pos, int typ) throws IOException + { + return 0; + } + + /** + * @see NetworkFile#truncateFile(long) + */ + public void truncateFile(long siz) throws IOException + { + } + + /** + * @see NetworkFile#writeFile(byte[], int, int, long) + */ + public void writeFile(byte[] buf, int len, int pos, long fileOff) throws IOException + { + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeHandler.java b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeHandler.java new file mode 100644 index 0000000000..635d30eac2 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCEPipeHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.server; + +/** + * DCE Pipe Handler Class + *

    + * Contains a list of the available DCE pipe handlers. + */ +public class DCEPipeHandler +{ + + // DCE/RPC pipe request handlers + + private static DCEHandler[] _handlers = { + new SrvsvcDCEHandler(), + null, // samr + null, // winreg + new WkssvcDCEHandler(), + null, // NETLOGON + null, // lsarpc + null, // spoolss + null, // netdfs + null, // service control + null, // eventlog + null // netlogon1 + }; + + /** + * Return the DCE/RPC request handler for the pipe type + * + * @param typ int + * @return DCEHandler + */ + public final static DCEHandler getHandlerForType(int typ) + { + if (typ >= 0 && typ < _handlers.length) + return _handlers[typ]; + return null; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/server/DCESrvPacket.java b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCESrvPacket.java new file mode 100644 index 0000000000..b2af9d20dd --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/server/DCESrvPacket.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.server; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.dcerpc.DCECommand; +import org.alfresco.filesys.smb.dcerpc.DCEDataPacker; +import org.alfresco.filesys.smb.server.SMBTransPacket; +import org.alfresco.filesys.util.DataPacker; + +/** + * DCE/RPC Server Packet Class + */ +public class DCESrvPacket extends SMBTransPacket +{ + + // DCE/RPC header offsets + + private static final int VERSIONMAJOR = 0; + private static final int VERSIONMINOR = 1; + private static final int PDUTYPE = 2; + private static final int HEADERFLAGS = 3; + private static final int PACKEDDATAREP = 4; + private static final int FRAGMENTLEN = 8; + private static final int AUTHLEN = 10; + private static final int CALLID = 12; + private static final int DCEDATA = 16; + + // DCE/RPC Request offsets + + private static final int ALLOCATIONHINT = 16; + private static final int PRESENTIDENT = 20; + private static final int OPERATIONID = 22; + private static final int OPERATIONDATA = 24; + + // Header flags + + public static final int FLG_FIRSTFRAG = 0x01; + public static final int FLG_LASTFRAG = 0x02; + public static final int FLG_ONLYFRAG = 0x03; + + // DCE/RPC header constants + + private static final byte HDR_VERSIONMAJOR = 5; + private static final byte HDR_VERSIONMINOR = 0; + private static final int HDR_PACKEDDATAREP = 0x00000010; + + // Offset to DCE/RPC header + + private int m_offset; + + /** + * Construct a DCE/RPC transaction packet + * + * @param buf Buffer that contains the SMB transaction packet. + */ + public DCESrvPacket(byte[] buf) + { + super(buf); + // m_offset = getParameterOffset(); + } + + /** + * Construct a DCE/RPC transaction packet + * + * @param siz Size of packet to allocate. + */ + public DCESrvPacket(int siz) + { + super(siz); + + // Set the multiplex id for this transaction + + setMultiplexId(getNextMultiplexId()); + } + + /** + * Return the major version number + * + * @return int + */ + public final int getMajorVersion() + { + return (int) (getBuffer()[m_offset + VERSIONMAJOR] & 0xFF); + } + + /** + * Return the minor version number + * + * @return int + */ + public final int getMinorVersion() + { + return (int) (getBuffer()[m_offset + VERSIONMINOR] & 0xFF); + } + + /** + * Return the PDU packet type + * + * @return int + */ + public final int getPDUType() + { + return (int) (getBuffer()[m_offset + PDUTYPE] & 0xFF); + } + + /** + * Return the header flags + * + * @return int + */ + public final int getHeaderFlags() + { + return (int) (getBuffer()[m_offset + HEADERFLAGS] & 0xFF); + } + + /** + * Return the packed data representation + * + * @return int + */ + public final int getPackedDataRepresentation() + { + return DataPacker.getIntelInt(getBuffer(), m_offset + PACKEDDATAREP); + } + + /** + * Return the fragment length + * + * @return int + */ + public final int getFragmentLength() + { + return DataPacker.getIntelShort(getBuffer(), m_offset + FRAGMENTLEN); + } + + /** + * Set the fragment length + * + * @param len int + */ + public final void setFragmentLength(int len) + { + + // Set the DCE header fragment length + + DataPacker.putIntelShort(len, getBuffer(), m_offset + FRAGMENTLEN); + } + + /** + * Return the authentication length + * + * @return int + */ + public final int getAuthenticationLength() + { + return DataPacker.getIntelShort(getBuffer(), m_offset + AUTHLEN); + } + + /** + * Return the call id + * + * @return int + */ + public final int getCallId() + { + return DataPacker.getIntelInt(getBuffer(), m_offset + CALLID); + } + + /** + * Determine if this is the first fragment + * + * @return boolean + */ + public final boolean isFirstFragment() + { + if ((getHeaderFlags() & FLG_FIRSTFRAG) != 0) + return true; + return false; + } + + /** + * Determine if this is the last fragment + * + * @return boolean + */ + public final boolean isLastFragment() + { + if ((getHeaderFlags() & FLG_LASTFRAG) != 0) + return true; + return false; + } + + /** + * Determine if this is the only fragment in the request + * + * @return boolean + */ + public final boolean isOnlyFragment() + { + if ((getHeaderFlags() & FLG_ONLYFRAG) == FLG_ONLYFRAG) + return true; + return false; + } + + /** + * Get the offset to the DCE/RPC data within the SMB packet + * + * @return int + */ + public final int getDCEDataOffset() + { + + // Determine the data offset from the DCE/RPC packet type + + int dataOff = -1; + switch (getPDUType()) + { + + // Bind/bind acknowledge + + case DCECommand.BIND: + case DCECommand.BINDACK: + dataOff = m_offset + DCEDATA; + break; + + // Request/response + + case DCECommand.REQUEST: + case DCECommand.RESPONSE: + dataOff = m_offset + OPERATIONDATA; + break; + } + + // Return the data offset + + return dataOff; + } + + /** + * Get the request allocation hint + * + * @return int + */ + public final int getAllocationHint() + { + return DataPacker.getIntelInt(getBuffer(), m_offset + ALLOCATIONHINT); + } + + /** + * Set the allocation hint + * + * @param alloc int + */ + public final void setAllocationHint(int alloc) + { + DataPacker.putIntelInt(alloc, getBuffer(), m_offset + ALLOCATIONHINT); + } + + /** + * Get the request presentation identifier + * + * @return int + */ + public final int getPresentationIdentifier() + { + return DataPacker.getIntelShort(getBuffer(), m_offset + PRESENTIDENT); + } + + /** + * Set the presentation identifier + * + * @param ident int + */ + public final void setPresentationIdentifier(int ident) + { + DataPacker.putIntelShort(ident, getBuffer(), m_offset + PRESENTIDENT); + } + + /** + * Get the request operation id + * + * @return int + */ + public final int getOperationId() + { + return DataPacker.getIntelShort(getBuffer(), m_offset + OPERATIONID); + } + + /** + * Initialize the DCE/RPC request. Set the SMB transaction parameter count so that the data + * offset can be calculated. + * + * @param handle int + * @param typ byte + * @param flags int + * @param callId int + */ + public final void initializeDCERequest(int handle, byte typ, int flags, int callId) + { + + // Initialize the transaction + + InitializeTransact(16, null, 0, null, 0); + + // Set the parameter byte count/offset for this packet + + int bytPos = DCEDataPacker.longwordAlign(getByteOffset()); + + setParameter(3, 0); + setParameter(4, bytPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Set the parameter displacement + + setParameter(5, 0); + + // Set the data byte count/offset for this packet + + setParameter(6, 0); + setParameter(7, bytPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Set the data displacement + + setParameter(8, 0); + + // Set up word count + + setParameter(9, 0); + + // Set the setup words + + setSetupParameter(0, PacketType.TransactNmPipe); + setSetupParameter(1, handle); + + // Reset the DCE offset for a DCE reply + + m_offset = bytPos; + + // Build the DCE/RPC header + + byte[] buf = getBuffer(); + DataPacker.putZeros(buf, m_offset, 24); + + buf[m_offset + VERSIONMAJOR] = HDR_VERSIONMAJOR; + buf[m_offset + VERSIONMINOR] = HDR_VERSIONMINOR; + buf[m_offset + PDUTYPE] = typ; + buf[m_offset + HEADERFLAGS] = (byte) (flags & 0xFF); + DataPacker.putIntelInt(HDR_PACKEDDATAREP, buf, m_offset + PACKEDDATAREP); + DataPacker.putIntelInt(0, buf, m_offset + AUTHLEN); + DataPacker.putIntelInt(callId, buf, m_offset + CALLID); + } + + /** + * Initialize the DCE/RPC reply. Set the SMB transaction parameter count so that the data offset + * can be calculated. + */ + public final void initializeDCEReply() + { + + // Set the total parameter words + + setParameterCount(10); + + // Set the total parameter/data bytes + + setParameter(0, 0); + setParameter(1, 0); + + // Set the parameter byte count/offset for this packet + + int bytPos = DCEDataPacker.longwordAlign(getByteOffset()); + + setParameter(3, 0); + setParameter(4, bytPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Set the parameter displacement + + setParameter(5, 0); + + // Set the data byte count/offset for this packet + + setParameter(6, 0); + setParameter(7, bytPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Set the data displacement + + setParameter(8, 0); + + // Set up word count + + setParameter(9, 0); + } + + /** + * Dump the DCE/RPC header details + */ + public final void DumpHeader() + { + + // Dump the PDU type + + System.out.println("** DCE/RPC Header - PDU Type = " + DCECommand.getCommandString(getPDUType())); + System.out.println(" Version : " + getMajorVersion() + "." + getMinorVersion()); + System.out.println(" Flags : 0x" + getHeaderFlags()); + System.out.println(" Packed Data Rep : 0x" + getPackedDataRepresentation()); + System.out.println(" Fragment Length : " + getFragmentLength()); + System.out.println(" Auth Length : " + getAuthenticationLength()); + System.out.println(" Call ID : " + getCallId()); + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/server/SrvsvcDCEHandler.java b/source/java/org/alfresco/filesys/smb/dcerpc/server/SrvsvcDCEHandler.java new file mode 100644 index 0000000000..05dd668777 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/server/SrvsvcDCEHandler.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.server; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Vector; + +import org.alfresco.filesys.server.auth.acl.AccessControlManager; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.alfresco.filesys.smb.Dialect; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.Srvsvc; +import org.alfresco.filesys.smb.dcerpc.info.ServerInfo; +import org.alfresco.filesys.smb.dcerpc.info.ShareInfo; +import org.alfresco.filesys.smb.dcerpc.info.ShareInfoList; +import org.alfresco.filesys.smb.server.SMBServer; +import org.alfresco.filesys.smb.server.SMBSrvException; +import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Srvsvc DCE/RPC Handler Class + */ +public class SrvsvcDCEHandler implements DCEHandler +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + /** + * Process a SrvSvc DCE/RPC request + * + * @param sess SMBSrvSession + * @param inBuf DCEBuffer + * @param pipeFile DCEPipeFile + * @exception IOException + * @exception SMBSrvException + */ + public void processRequest(SMBSrvSession sess, DCEBuffer inBuf, DCEPipeFile pipeFile) throws IOException, + SMBSrvException + { + + // Get the operation code and move the buffer pointer to the start of the request data + + int opNum = inBuf.getHeaderValue(DCEBuffer.HDR_OPCODE); + try + { + inBuf.skipBytes(DCEBuffer.OPERATIONDATA); + } + catch (DCEBufferException ex) + { + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("DCE/RPC SrvSvc request=" + Srvsvc.getOpcodeName(opNum)); + + // Create the output DCE buffer and add the response header + + DCEBuffer outBuf = new DCEBuffer(); + outBuf.putResponseHeader(inBuf.getHeaderValue(DCEBuffer.HDR_CALLID), 0); + + // Process the request + + boolean processed = false; + + switch (opNum) + { + + // Enumerate shares + + case Srvsvc.NetrShareEnum: + processed = netShareEnum(sess, inBuf, outBuf); + break; + + // Enumerate all shares + + case Srvsvc.NetrShareEnumSticky: + processed = netShareEnum(sess, inBuf, outBuf); + break; + + // Get share information + + case Srvsvc.NetrShareGetInfo: + processed = netShareGetInfo(sess, inBuf, outBuf); + break; + + // Get server information + + case Srvsvc.NetrServerGetInfo: + processed = netServerGetInfo(sess, inBuf, outBuf); + break; + + // Unsupported function + + default: + break; + } + + // Return an error status if the request was not processed + + if (processed == false) + { + sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Set the allocation hint for the response + + outBuf.setHeaderValue(DCEBuffer.HDR_ALLOCHINT, outBuf.getLength()); + + // Attach the output buffer to the pipe file + + pipeFile.setBufferedData(outBuf); + } + + /** + * Handle a share enumeration request + * + * @param sess SMBSrvSession + * @param inBuf DCEPacket + * @param outBuf DCEPacket + * @return boolean + */ + protected final boolean netShareEnum(SMBSrvSession sess, DCEBuffer inBuf, DCEBuffer outBuf) + { + + // Decode the request + + String srvName = null; + ShareInfoList shrInfo = null; + + try + { + inBuf.skipPointer(); + srvName = inBuf.getString(DCEBuffer.ALIGN_INT); + shrInfo = new ShareInfoList(inBuf); + } + catch (DCEBufferException ex) + { + return false; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("NetShareEnum srvName=" + srvName + ", shrInfo=" + shrInfo.toString()); + + // Get the share list from the server + + SharedDeviceList shareList = sess.getServer().getShareMapper().getShareList(srvName, sess, false); + + // Check if there is an access control manager configured + + if (sess.getServer().hasAccessControlManager()) + { + + // Filter the list of available shares by applying any access control rules + + AccessControlManager aclMgr = sess.getServer().getAccessControlManager(); + + shareList = aclMgr.filterShareList(sess, shareList); + } + + // Create a list of share information objects of the required information level + + Vector infoList = new Vector(); + Enumeration enm = shareList.enumerateShares(); + + while (enm.hasMoreElements()) + { + + // Get the current shared device details + + SharedDevice share = enm.nextElement(); + + // Determine the share type + + int shrTyp = ShareInfo.Disk; + + if (share.getType() == ShareType.PRINTER) + shrTyp = ShareInfo.PrintQueue; + else if (share.getType() == ShareType.NAMEDPIPE) + shrTyp = ShareInfo.IPC; + else if (share.getType() == ShareType.ADMINPIPE) + shrTyp = ShareInfo.IPC + ShareInfo.Hidden; + + // Create a share information object with the basic information + + ShareInfo info = new ShareInfo(shrInfo.getInformationLevel(), share.getName(), shrTyp, share.getComment()); + infoList.add(info); + + // Add additional information + + switch (shrInfo.getInformationLevel()) + { + + // Level 2 + + case 2: + if (share.getContext() != null) + info.setPath(share.getContext().getDeviceName()); + break; + + // Level 502 + + case 502: + if (share.getContext() != null) + info.setPath(share.getContext().getDeviceName()); + break; + } + } + + // Set the share information list in the server share information and write the + // share information to the output DCE buffer. + + shrInfo.setShareList(infoList); + try + { + shrInfo.writeList(outBuf); + outBuf.putInt(0); // status code + } + catch (DCEBufferException ex) + { + } + + // Indicate that the request was processed successfully + + return true; + } + + /** + * Handle a get share information request + * + * @param sess SMBSrvSession + * @param inBuf DCEPacket + * @param outBuf DCEPacket + * @return boolean + */ + protected final boolean netShareGetInfo(SMBSrvSession sess, DCEBuffer inBuf, DCEBuffer outBuf) + { + + // Decode the request + + String srvName = null; + String shrName = null; + int infoLevel = 0; + + try + { + inBuf.skipPointer(); + srvName = inBuf.getString(DCEBuffer.ALIGN_INT); + shrName = inBuf.getString(DCEBuffer.ALIGN_INT); + infoLevel = inBuf.getInt(); + } + catch (DCEBufferException ex) + { + return false; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("netShareGetInfo srvname=" + srvName + ", share=" + shrName + ", infoLevel=" + infoLevel); + + // Find the required shared device + + SharedDevice share = null; + + try + { + + // Get the shared device details + + share = sess.getServer().findShare(srvName, shrName, ShareType.UNKNOWN, sess, false); + } + catch (Exception ex) + { + } + + // Check if the share details are valid + + if (share == null) + return false; + + // Determine the share type + + int shrTyp = ShareInfo.Disk; + + if (share.getType() == ShareType.PRINTER) + shrTyp = ShareInfo.PrintQueue; + else if (share.getType() == ShareType.NAMEDPIPE) + shrTyp = ShareInfo.IPC; + else if (share.getType() == ShareType.ADMINPIPE) + shrTyp = ShareInfo.IPC + ShareInfo.Hidden; + + // Create the share information + + ShareInfo shrInfo = new ShareInfo(infoLevel, share.getName(), shrTyp, share.getComment()); + + // Pack the information level, structure pointer and share information + + outBuf.putInt(infoLevel); + outBuf.putPointer(true); + + shrInfo.writeObject(outBuf, outBuf); + + // Add the status and return a success status + + outBuf.putInt(0); + return true; + } + + /** + * Handle a get server information request + * + * @param sess SMBSrvSession + * @param inBuf DCEPacket + * @param outBuf DCEPacket + * @return boolean + */ + protected final boolean netServerGetInfo(SMBSrvSession sess, DCEBuffer inBuf, DCEBuffer outBuf) + { + + // Decode the request + + String srvName = null; + int infoLevel = 0; + + try + { + inBuf.skipPointer(); + srvName = inBuf.getString(DCEBuffer.ALIGN_INT); + infoLevel = inBuf.getInt(); + } + catch (DCEBufferException ex) + { + return false; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("netServerGetInfo srvname=" + srvName + ", infoLevel=" + infoLevel); + + // Create the server information and set the common values + + ServerInfo srvInfo = new ServerInfo(infoLevel); + + SMBServer srv = sess.getSMBServer(); + srvInfo.setServerName(srv.getServerName()); + srvInfo.setComment(srv.getComment()); + srvInfo.setServerType(srv.getServerType()); + + // Determine if the server is using the NT SMB dialect and set the platofmr id accordingly + + ServerConfiguration srvConfig = srv.getConfiguration(); + if (srvConfig != null && srvConfig.getEnabledDialects().hasDialect(Dialect.NT) == true) + { + srvInfo.setPlatformId(ServerInfo.PLATFORM_NT); + srvInfo.setVersion(5, 1); + } + else + { + srvInfo.setPlatformId(ServerInfo.PLATFORM_OS2); + srvInfo.setVersion(4, 0); + } + + // Write the server information to the DCE response + + srvInfo.writeObject(outBuf, outBuf); + outBuf.putInt(0); + + // Indicate that the request was processed successfully + + return true; + } +} diff --git a/source/java/org/alfresco/filesys/smb/dcerpc/server/WkssvcDCEHandler.java b/source/java/org/alfresco/filesys/smb/dcerpc/server/WkssvcDCEHandler.java new file mode 100644 index 0000000000..3cf49bee8b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/dcerpc/server/WkssvcDCEHandler.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.dcerpc.server; + +import java.io.IOException; + +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.smb.Dialect; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.Wkssvc; +import org.alfresco.filesys.smb.dcerpc.info.ServerInfo; +import org.alfresco.filesys.smb.dcerpc.info.WorkstationInfo; +import org.alfresco.filesys.smb.server.SMBServer; +import org.alfresco.filesys.smb.server.SMBSrvException; +import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Wkssvc DCE/RPC Handler Class + */ +public class WkssvcDCEHandler implements DCEHandler +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + /** + * Process a WksSvc DCE/RPC request + * + * @param sess SMBSrvSession + * @param inBuf DCEBuffer + * @param pipeFile DCEPipeFile + * @exception IOException + * @exception SMBSrvException + */ + public void processRequest(SMBSrvSession sess, DCEBuffer inBuf, DCEPipeFile pipeFile) throws IOException, + SMBSrvException + { + + // Get the operation code and move the buffer pointer to the start of the request data + + int opNum = inBuf.getHeaderValue(DCEBuffer.HDR_OPCODE); + try + { + inBuf.skipBytes(DCEBuffer.OPERATIONDATA); + } + catch (DCEBufferException ex) + { + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("DCE/RPC WksSvc request=" + Wkssvc.getOpcodeName(opNum)); + + // Create the output DCE buffer and add the response header + + DCEBuffer outBuf = new DCEBuffer(); + outBuf.putResponseHeader(inBuf.getHeaderValue(DCEBuffer.HDR_CALLID), 0); + + // Process the request + + boolean processed = false; + + switch (opNum) + { + + // Get workstation information + + case Wkssvc.NetWkstaGetInfo: + processed = netWkstaGetInfo(sess, inBuf, outBuf); + break; + + // Unsupported function + + default: + break; + } + + // Return an error status if the request was not processed + + if (processed == false) + { + sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Set the allocation hint for the response + + outBuf.setHeaderValue(DCEBuffer.HDR_ALLOCHINT, outBuf.getLength()); + + // Attach the output buffer to the pipe file + + pipeFile.setBufferedData(outBuf); + } + + /** + * Get workstation infomation + * + * @param sess SMBSrvSession + * @param inBuf DCEPacket + * @param outBuf DCEPacket + * @return boolean + */ + protected final boolean netWkstaGetInfo(SMBSrvSession sess, DCEBuffer inBuf, DCEBuffer outBuf) + { + + // Decode the request + + String srvName = null; + int infoLevel = 0; + + try + { + inBuf.skipPointer(); + srvName = inBuf.getString(DCEBuffer.ALIGN_INT); + infoLevel = inBuf.getInt(); + } + catch (DCEBufferException ex) + { + return false; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("NetWkstaGetInfo srvName=" + srvName + ", infoLevel=" + infoLevel); + + // Create the workstation information and set the common values + + WorkstationInfo wkstaInfo = new WorkstationInfo(infoLevel); + + SMBServer srv = sess.getSMBServer(); + wkstaInfo.setWorkstationName(srv.getServerName()); + wkstaInfo.setDomain(srv.getConfiguration().getDomainName()); + + // Determine if the server is using the NT SMB dialect and set the platofmr id accordingly + + ServerConfiguration srvConfig = sess.getServer().getConfiguration(); + if (srvConfig != null && srvConfig.getEnabledDialects().hasDialect(Dialect.NT) == true) + { + wkstaInfo.setPlatformId(ServerInfo.PLATFORM_NT); + wkstaInfo.setVersion(5, 1); + } + else + { + wkstaInfo.setPlatformId(ServerInfo.PLATFORM_OS2); + wkstaInfo.setVersion(4, 0); + } + + // Write the server information to the DCE response + + wkstaInfo.writeObject(outBuf, outBuf); + outBuf.putInt(0); + + // Indicate that the request was processed successfully + + return true; + } +} diff --git a/source/java/org/alfresco/filesys/smb/mailslot/HostAnnouncer.java b/source/java/org/alfresco/filesys/smb/mailslot/HostAnnouncer.java new file mode 100644 index 0000000000..73243fa2ed --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/mailslot/HostAnnouncer.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.mailslot; + +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.smb.ServerType; +import org.alfresco.filesys.smb.TransactionNames; +import org.alfresco.filesys.util.StringList; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

    + * The host announcer class periodically broadcasts a host announcement datagram to inform other + * Windows networking hosts of the local hosts existence and capabilities. + */ +public abstract class HostAnnouncer extends Thread +{ + + // Debug logging + + protected static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol.mailslot"); + + // Shutdown announcement interval and message count + + public static final int SHUTDOWN_WAIT = 2000; // 2 seconds + public static final int SHUTDOWN_COUNT = 3; + + // Starting announcement interval, doubles until it reaches the configured interval + + public static final long STARTING_INTERVAL = 5000; // 5 seconds + + // Local host name(s) to announce + + private StringList m_names; + + // Domain to announce to + + private String m_domain; + + // Server comment string + + private String m_comment; + + // Announcement interval in minutes + + private int m_interval; + + // Server type flags, see org.alfresco.filesys.smb.SMBServerInfo + + private int m_srvtype = ServerType.WorkStation + ServerType.Server; + + // SMB mailslot packet + + private SMBMailslotPacket m_smbPkt; + + // Update count for the host announcement packet + + private byte m_updateCount; + + // Shutdown flag, host announcer should remove the announced name as it shuts down + + private boolean m_shutdown = false; + + // Debug output enable + + private boolean m_debug; + + /** + * HostAnnouncer constructor. + */ + public HostAnnouncer() + { + + // Common constructor + + commonConstructor(); + } + + /** + * Create a host announcer. + * + * @param name Host name to announce + * @param domain Domain name to announce to + * @param intval Announcement interval, in minutes + */ + public HostAnnouncer(String name, String domain, int intval) + { + + // Common constructor + + commonConstructor(); + + // Add the host to the list of names to announce + + addHostName(name); + setDomain(domain); + setInterval(intval); + } + + /** + * Common constructor code + */ + private final void commonConstructor() + { + + // Allocate the host name list + + m_names = new StringList(); + } + + /** + * Return the server comment string. + * + * @return java.lang.String + */ + public final String getComment() + { + return m_comment; + } + + /** + * Return the domain name that the host announcement is directed to. + * + * @return java.lang.String + */ + public final String getDomain() + { + return m_domain; + } + + /** + * Return the number of names being announced + * + * @return int + */ + public final int numberOfNames() + { + return m_names.numberOfStrings(); + } + + /** + * Return the specified host name being announced. + * + * @param idx int + * @return java.lang.String + */ + public final String getHostName(int idx) + { + if (idx < 0 || idx > m_names.numberOfStrings()) + return null; + return m_names.getStringAt(idx); + } + + /** + * Return the announcement interval, in minutes. + * + * @return int + */ + public final int getInterval() + { + return m_interval; + } + + /** + * Return the server type flags. + * + * @return int + */ + public final int getServerType() + { + return m_srvtype; + } + + /** + * Determine if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Enable/disable debug output + * + * @param dbg true or false + */ + public final void setDebug(boolean dbg) + { + m_debug = dbg; + } + + /** + * Initialize the host announcement SMB. + * + * @param name String + */ + protected final void initHostAnnounceSMB(String name) + { + + // Allocate the transact SMB + + if (m_smbPkt == null) + m_smbPkt = new SMBMailslotPacket(); + + // Create the host announcement structure + + byte[] data = new byte[256]; + int pos = MailSlot.createHostAnnouncement(data, 0, name, m_comment, m_srvtype, m_interval, m_updateCount++); + + // Create the mailslot SMB + + m_smbPkt.initializeMailslotSMB(TransactionNames.MailslotBrowse, data, pos); + } + + /** + * Start the host announcer thread. + */ + public void run() + { + + // Initialize the host announcer + + try + { + + // Initialize the host announcer datagram socket + + initialize(); + } + catch (Exception ex) + { + + // Debug + + logger.error("HostAnnouncer initialization error", ex); + return; + } + + // Clear the shutdown flag + + m_shutdown = false; + + // Send the host announcement datagram + + long sleepTime = STARTING_INTERVAL; + long sleepNormal = getInterval() * 60 * 1000; + + while (m_shutdown == false) + { + + try + { + + // Check if the network connection is valid + + if (isNetworkEnabled()) + { + + // Loop through the host names to be announced + + for (int i = 0; i < m_names.numberOfStrings(); i++) + { + + // Create a host announcement transact SMB + + String hostName = getHostName(i); + initHostAnnounceSMB(hostName); + + // Send the host announce datagram + + sendAnnouncement(hostName, m_smbPkt.getBuffer(), 0, m_smbPkt.getLength()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("HostAnnouncer: Announced host " + hostName); + } + } + else + { + + // Reset the sleep interval to the starting interval as the network connection + // is not + // available + + sleepTime = STARTING_INTERVAL; + } + + // Sleep for a while + + sleep(sleepTime); + + // Update the sleep interval, if the network connection is enabled + + if (isNetworkEnabled() && sleepTime < sleepNormal) + { + + // Double the sleep interval until it exceeds the configured announcement + // interval. + // This is to send out more broadcasts when the server first starts. + + sleepTime *= 2; + if (sleepTime > sleepNormal) + sleepTime = sleepNormal; + } + } + catch (Exception ex) + { + + // Debug + + if (m_shutdown == false) + logger.error("HostAnnouncer error", ex); + m_shutdown = true; + } + } + + // Set the announcement interval to zero to indicate that the host is leaving Network + // Neighborhood + + setInterval(0); + + // Clear the server flag in the announced host type + + if ((m_srvtype & ServerType.Server) != 0) + m_srvtype -= ServerType.Server; + + // Send out a number of host announcement to remove the host name(s) from Network + // Neighborhood + + for (int j = 0; j < SHUTDOWN_COUNT; j++) + { + + // Loop through the host names to be announced + + for (int i = 0; i < m_names.numberOfStrings(); i++) + { + + // Create a host announcement transact SMB + + String hostName = getHostName(i); + initHostAnnounceSMB(hostName); + + // Send the host announce datagram + + try + { + + // Send the host announcement + + sendAnnouncement(hostName, m_smbPkt.getBuffer(), 0, m_smbPkt.getLength()); + } + catch (Exception ex) + { + } + } + + // Sleep for a while + + try + { + sleep(SHUTDOWN_WAIT); + } + catch (InterruptedException ex) + { + } + } + } + + /** + * Initialize the host announcer. + * + * @exception Exception + */ + protected void initialize() throws Exception + { + } + + /** + * Determine if the network connection used for the host announcement is valid + * + * @return boolean + */ + public abstract boolean isNetworkEnabled(); + + /** + * Send an announcement broadcast. + * + * @param hostName Host name being announced + * @param buf Buffer containing the host announcement mailslot message. + * @param offset Offset to the start of the host announcement message. + * @param len Host announcement message length. + */ + protected abstract void sendAnnouncement(String hostName, byte[] buf, int offset, int len) throws Exception; + + /** + * Set the server comment string. + * + * @param comment java.lang.String + */ + public final void setComment(String comment) + { + m_comment = comment; + if (m_comment != null && m_comment.length() > 80) + m_comment = m_comment.substring(0, 80); + } + + /** + * Set the domain name that the host announcement are directed to. + * + * @param name java.lang.String + */ + public final void setDomain(String name) + { + m_domain = name.toUpperCase(); + } + + /** + * Add a host name to the list of names to announce + * + * @param name java.lang.String + */ + public final void addHostName(String name) + { + m_names.addString(NetBIOSName.toUpperCaseName(name)); + } + + /** + * Add a list of names to the announcement list + * + * @param names StringList + */ + public final void addHostNames(StringList names) + { + m_names.addStrings(names); + } + + /** + * Set the announcement interval, in minutes. + * + * @param intval int + */ + public final void setInterval(int intval) + { + m_interval = intval; + } + + /** + * Set the server type flags. + * + * @param typ int + */ + public final void setServerType(int typ) + { + m_srvtype = typ; + } + + /** + * Shutdown the host announcer and remove the announced name from Network Neighborhood. + */ + public final synchronized void shutdownAnnouncer() + { + + // Set the shutdown flag and wakeup the main host announcer thread + + m_shutdown = true; + interrupt(); + + try + { + join(2000); + } + catch (InterruptedException ex) + { + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/mailslot/MailSlot.java b/source/java/org/alfresco/filesys/smb/mailslot/MailSlot.java new file mode 100644 index 0000000000..bf07a74360 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/mailslot/MailSlot.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.mailslot; + +import org.alfresco.filesys.util.DataPacker; + +/** + * Mail slot constants class. + */ +public final class MailSlot +{ + + // Mail slot opcodes + + public static final int WRITE = 0x01; + + // Mail slot classes + + public static final int UNRELIABLE = 0x02; + + // Mailslot \MAILSLOT\BROWSE opcodes + + public static final int HostAnnounce = 1; + public static final int AnnouncementRequest = 2; + public static final int RequestElection = 8; + public static final int GetBackupListReq = 9; + public static final int GetBackupListResp = 10; + public static final int BecomeBackup = 11; + public static final int DomainAnnouncement = 12; + public static final int MasterAnnouncement = 13; + public static final int LocalMasterAnnouncement = 15; + + /** + * Create a host announcement mailslot structure + * + * @param buf byte[] + * @param off int + * @param host String + * @param comment String + * @param typ int + * @param interval int + * @param upd int + * @return int + */ + public final static int createHostAnnouncement(byte[] buf, int off, String host, String comment, int typ, + int interval, int upd) + { + + // Set the command code and update count + + buf[off] = MailSlot.HostAnnounce; + buf[off + 1] = 0; // (byte) (upd & 0xFF); + + // Set the announce interval, in minutes + + DataPacker.putIntelInt(interval * 60000, buf, off + 2); + + // Pack the host name + + byte[] hostByt = host.getBytes(); + for (int i = 0; i < 16; i++) + { + if (i < hostByt.length) + buf[off + 6 + i] = hostByt[i]; + else + buf[off + 6 + i] = 0; + } + + // Major/minor version number + + buf[off + 22] = 5; // major version + buf[off + 23] = 1; // minor version + + // Set the server type flags + + DataPacker.putIntelInt(typ, buf, off + 24); + + // Browser election version and browser constant + + DataPacker.putIntelShort(0x010F, buf, off + 28); + DataPacker.putIntelShort(0xAA55, buf, off + 30); + + // Add the server comment string, or a null string + + int pos = off + 33; + + if (comment != null) + pos = DataPacker.putString(comment, buf, off + 32, true); + + // Return the end of data position + + return pos; + } + + /** + * Create an announcement request mailslot structure + * + * @param buf byte[] + * @param off int + * @param host String + * @return int + */ + public final static int createAnnouncementRequest(byte[] buf, int off, String host) + { + + // Set the command code + + buf[off] = MailSlot.AnnouncementRequest; + buf[off + 1] = 0; + + // Pack the host name + + byte[] hostByt = host.getBytes(); + for (int i = 0; i < 16; i++) + { + if (i < hostByt.length) + buf[off + 2 + i] = hostByt[i]; + else + buf[off + 2 + i] = 0; + } + + // Return the end of buffer position + + return off + 17; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/mailslot/SMBMailslotPacket.java b/source/java/org/alfresco/filesys/smb/mailslot/SMBMailslotPacket.java new file mode 100644 index 0000000000..4db4d9a3c2 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/mailslot/SMBMailslotPacket.java @@ -0,0 +1,985 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.mailslot; + +import org.alfresco.filesys.util.DataPacker; + +/** + * SMB Mailslot Packet Class + */ +public class SMBMailslotPacket +{ + // SMB packet offsets + + public static final int SIGNATURE = 0; + public static final int COMMAND = 4; + public static final int ERRORCODE = 5; + public static final int ERRORCLASS = 5; + public static final int ERROR = 7; + public static final int FLAGS = 9; + public static final int FLAGS2 = 10; + public static final int PIDHIGH = 12; + public static final int SID = 18; + public static final int SEQNO = 20; + public static final int TID = 24; + public static final int PID = 26; + public static final int UID = 28; + public static final int MID = 30; + public static final int WORDCNT = 32; + public static final int ANDXCOMMAND = 33; + public static final int ANDXRESERVED= 34; + public static final int PARAMWORDS = 33; + + // SMB packet header length for a transaction type request + + public static final int TRANS_HEADERLEN = 66; + + // Minimum receive length for a valid SMB packet + + public static final int MIN_RXLEN = 32; + + // Default buffer size to allocate for SMB mailslot packets + + public static final int DEFAULT_BUFSIZE = 500; + + // Flag bits + + public static final int FLG_SUBDIALECT = 0x01; + public static final int FLG_CASELESS = 0x08; + public static final int FLG_CANONICAL = 0x10; + public static final int FLG_OPLOCK = 0x20; + public static final int FLG_NOTIFY = 0x40; + public static final int FLG_RESPONSE = 0x80; + + // Flag2 bits + + public static final int FLG2_LONGFILENAMES = 0x0001; + public static final int FLG2_EXTENDEDATTRIB = 0x0002; + public static final int FLG2_READIFEXE = 0x2000; + public static final int FLG2_LONGERRORCODE = 0x4000; + public static final int FLG2_UNICODE = 0x8000; + + // SMB packet buffer and offset + + private byte[] m_smbbuf; + private int m_offset; + + // Define the number of standard parameters for a server response + + private static final int STD_PARAMS = 14; + + // SMB packet types we expect to receive in a mailslot + + public static final int Transaction = 0x25; + public static final int Transaction2 = 0x32; + + /** + * Default constructor + */ + public SMBMailslotPacket() + { + m_smbbuf = new byte[DEFAULT_BUFSIZE]; + m_offset = 0; + } + + /** + * Class constructor + * + * @param buf byte[] + */ + public SMBMailslotPacket(byte[] buf) + { + m_smbbuf = buf; + m_offset = 0; + } + + /** + * Class constructor + * + * @param buf byte[] + * @param off int + */ + public SMBMailslotPacket(byte[] buf, int off) + { + m_smbbuf = buf; + m_offset = off; + } + + /** + * Reset the mailslot packet to use the specified buffer and offset + * + * @param buf byte[] + * @param offset int + */ + public final void resetPacket(byte[] buf, int offset) + { + m_smbbuf = buf; + m_offset = offset; + } + + /** + * Get the secondary command code + * + * @return Secondary command code + */ + public final int getAndXCommand() + { + return (int) (m_smbbuf[ANDXCOMMAND + m_offset] & 0xFF); + } + + /** + * Return the byte array used for the SMB packet + * + * @return Byte array used for the SMB packet. + */ + public final byte[] getBuffer() + { + return m_smbbuf; + } + + /** + * Return the total buffer size available to the SMB request + * + * @return Total SMB buffer length available. + */ + public final int getBufferLength() + { + return m_smbbuf.length - m_offset; + } + + /** + * Get the data byte count for the SMB packet + * + * @return Data byte count + */ + public final int getByteCount() + { + + // Calculate the offset of the byte count + + int pos = PARAMWORDS + (2 * getParameterCount()); + return (int) DataPacker.getIntelShort(m_smbbuf, pos); + } + + /** + * Get the data byte area offset within the SMB packet + * + * @return Data byte offset within the SMB packet. + */ + public final int getByteOffset() + { + + // Calculate the offset of the byte buffer + + int pCnt = getParameterCount(); + int pos = WORDCNT + (2 * pCnt) + 3 + m_offset; + return pos; + } + + /** + * Get the SMB command + * + * @return SMB command code. + */ + public final int getCommand() + { + return (int) (m_smbbuf[COMMAND + m_offset] & 0xFF); + } + + /** + * Determine if normal or long error codes have been returned + * + * @return boolean + */ + public final boolean hasLongErrorCode() + { + if ((getFlags2() & FLG2_LONGERRORCODE) == 0) + return false; + return true; + } + + /** + * Get the SMB error class + * + * @return SMB error class. + */ + public final int getErrorClass() + { + return (int) m_smbbuf[ERRORCLASS + m_offset] & 0xFF; + } + + /** + * Get the SMB error code + * + * @return SMB error code. + */ + public final int getErrorCode() + { + return (int) m_smbbuf[ERROR + m_offset] & 0xFF; + } + + /** + * Get the SMB flags value. + * + * @return SMB flags value. + */ + public final int getFlags() + { + return (int) m_smbbuf[FLAGS + m_offset] & 0xFF; + } + + /** + * Get the SMB flags2 value. + * + * @return SMB flags2 value. + */ + public final int getFlags2() + { + return (int) DataPacker.getIntelShort(m_smbbuf, FLAGS2 + m_offset); + } + + /** + * Calculate the total used packet length. + * + * @return Total used packet length. + */ + public final int getLength() + { + return (getByteOffset() + getByteCount()) - m_offset; + } + + /** + * Get the long SMB error code + * + * @return Long SMB error code. + */ + public final int getLongErrorCode() + { + return DataPacker.getIntelInt(m_smbbuf, ERRORCODE + m_offset); + } + + /** + * Get the multiplex identifier. + * + * @return Multiplex identifier. + */ + public final int getMultiplexId() + { + return DataPacker.getIntelShort(m_smbbuf, MID + m_offset); + } + + /** + * Get a parameter word from the SMB packet. + * + * @param idx Parameter index (zero based). + * @return Parameter word value. + * @exception java.lang.IndexOutOfBoundsException If the parameter index is out of range. + */ + public final int getParameter(int idx) throws java.lang.IndexOutOfBoundsException + { + + // Range check the parameter index + + if (idx > getParameterCount()) + throw new java.lang.IndexOutOfBoundsException(); + + // Calculate the parameter word offset + + int pos = WORDCNT + (2 * idx) + 1 + m_offset; + return (int) (DataPacker.getIntelShort(m_smbbuf, pos) & 0xFFFF); + } + + /** + * Get the parameter count + * + * @return Parameter word count. + */ + public final int getParameterCount() + { + return (int) m_smbbuf[WORDCNT + m_offset]; + } + + /** + * Get the process indentifier (PID) + * + * @return Process identifier value. + */ + public final int getProcessId() + { + return DataPacker.getIntelShort(m_smbbuf, PID + m_offset); + } + + /** + * Get the tree identifier (TID) + * + * @return Tree identifier (TID) + */ + public final int getTreeId() + { + return DataPacker.getIntelShort(m_smbbuf, TID + m_offset); + } + + /** + * Get the user identifier (UID) + * + * @return User identifier (UID) + */ + public final int getUserId() + { + return DataPacker.getIntelShort(m_smbbuf, UID + m_offset); + } + + /** + * Return the offset to the data block within the SMB packet. The data block is word aligned + * within the byte buffer area of the SMB packet. This method must be called after the parameter + * count and parameter block length have been set. + * + * @return int Offset to the data block area. + */ + public final int getDataBlockOffset() + { + + // Get the position of the parameter block + + int pos = (getParameterBlockOffset() + getParameter(3)) + m_offset; + if ((pos & 0x01) != 0) + pos++; + return pos; + } + + /** + * Return the offset to the data block within the SMB packet. The data block is word aligned + * within the byte buffer area of the SMB packet. This method must be called after the parameter + * count has been set. + * + * @param prmLen Parameter block length, in bytes. + * @return int Offset to the data block area. + */ + public final int getDataBlockOffset(int prmLen) + { + + // Get the position of the parameter block + + int pos = getParameterBlockOffset() + prmLen; + if ((pos & 0x01) != 0) + pos++; + return pos; + } + + /** + * Return the parameter block offset where the parameter bytes should be placed. This method + * must be called after the paramter count has been set. The parameter offset is word aligned. + * + * @return int Offset to the parameter block area. + */ + public final int getParameterBlockOffset() + { + + // Get the offset to the byte buffer area of the SMB packet + + int pos = getByteOffset() + m_offset; + if ((pos & 0x01) != 0) + pos++; + return pos; + } + + /** + * Return the data block offset. + * + * @return int Offset to data block within packet. + */ + public final int getRxDataBlock() + { + return getParameter(12) + m_offset; + } + + /** + * Return the received transaction data block length. + * + * @return int + */ + public final int getRxDataBlockLength() + { + return getParameter(11); + } + + /** + * Get the required transact parameter word (16 bit). + * + * @param prmIdx int + * @return int + */ + public final int getRxParameter(int prmIdx) + { + + // Get the parameter block offset + + int pos = getRxParameterBlock(); + + // Get the required transact parameter word. + + pos += prmIdx * 2; // 16 bit words + return DataPacker.getIntelShort(getBuffer(), pos); + } + + /** + * Return the position of the parameter block within the received packet. + * + * @param prmblk Array to unpack the parameter block words into. + */ + public final int getRxParameterBlock() + { + + // Get the offset to the parameter words + + return getParameter(10) + m_offset; + } + + /** + * Return the received transaction parameter block length. + * + * @return int + */ + public final int getRxParameterBlockLength() + { + return getParameter(9); + } + + /** + * Return the received transaction setup parameter count. + * + * @return int + */ + public final int getRxParameterCount() + { + return getParameterCount() - STD_PARAMS; + } + + /** + * Get the required transact parameter int value (32-bit). + * + * @param prmIdx int + * @return int + */ + public final int getRxParameterInt(int prmIdx) + { + + // Get the parameter block offset + + int pos = getRxParameterBlock(); + + // Get the required transact parameter word. + + pos += prmIdx * 2; // 16 bit words + return DataPacker.getIntelInt(getBuffer(), pos); + } + + /** + * Get the required transact parameter string. + * + * @param pos Offset to the string within the parameter block. + * @return int + */ + public final String getRxParameterString(int pos) + { + + // Get the parameter block offset + + pos += getRxParameterBlock(); + + // Get the transact parameter string + + byte[] buf = getBuffer(); + int len = (buf[pos++] & 0x00FF); + return DataPacker.getString(buf, pos, len); + } + + /** + * Get the required transact parameter string. + * + * @param pos Offset to the string within the parameter block. + * @param len Length of the string. + * @return int + */ + public final String getRxParameterString(int pos, int len) + { + + // Get the parameter block offset + + pos += getRxParameterBlock(); + + // Get the transact parameter string + + byte[] buf = getBuffer(); + return DataPacker.getString(buf, pos, len); + } + + /** + * Return the received transaction name. + * + * @return java.lang.String + */ + public final String getRxTransactName() + { + + // Check if the transaction has a name + + if (getCommand() == Transaction2) + return ""; + + // Unpack the transaction name string + + int pos = getByteOffset(); + return DataPacker.getString(getBuffer(), pos, getByteCount()); + } + + /** + * Return the specified transaction setup parameter. + * + * @param idx Setup parameter index. + */ + public final int getSetupParameter(int idx) throws java.lang.ArrayIndexOutOfBoundsException + { + + // Check if the setup parameter index is valid + + if (idx >= getRxParameterCount()) + throw new java.lang.ArrayIndexOutOfBoundsException(); + + // Get the setup parameter + + return getParameter(idx + STD_PARAMS); + } + + /** + * Return the mailslot opcode + * + * @return int + */ + public final int getMailslotOpcode() + { + try + { + return getSetupParameter(0); + } + catch (ArrayIndexOutOfBoundsException ex) + { + } + return -1; + } + + /** + * Return the mailslot priority + * + * @return int + */ + public final int getMailslotPriority() + { + try + { + return getSetupParameter(1); + } + catch (ArrayIndexOutOfBoundsException ex) + { + } + return -1; + } + + /** + * Return the mailslot class of service + * + * @return int + */ + public final int getMailslotClass() + { + try + { + return getSetupParameter(2); + } + catch (ArrayIndexOutOfBoundsException ex) + { + } + return -1; + } + + /** + * Return the mailslot sub-opcode, the first byte from the mailslot data + * + * @return int + */ + public final int getMailslotSubOpcode() + { + return (int) (m_smbbuf[getMailslotDataOffset()] & 0xFF); + } + + /** + * Return the mailslot data offset + * + * @return int + */ + public final int getMailslotDataOffset() + { + return getRxDataBlock(); + } + + /** + * Initialize a mailslot SMB + * + * @param name Mailslot name + * @param data Request data bytes + * @param dlen Data length + */ + public final void initializeMailslotSMB(String name, byte[] data, int dlen) + { + + // Initialize the SMB packet header + + initializeBuffer(); + + // Clear header values + + setFlags(0); + setFlags2(0); + setUserId(0); + setMultiplexId(0); + setTreeId(0); + setProcessId(0); + + // Initialize the transaction + + initializeTransact(name, 17, null, 0, data, dlen); + + // Initialize the transactin setup parameters for a mailslot write + + setSetupParameter(0, MailSlot.WRITE); + setSetupParameter(1, 1); + setSetupParameter(2, MailSlot.UNRELIABLE); + } + + /** + * Initialize the transact SMB packet + * + * @param name Transaction name + * @param pcnt Total parameter count for this transaction + * @param paramblk Parameter block data bytes + * @param plen Parameter block data length + * @param datablk Data block data bytes + * @param dlen Data block data length + */ + protected final void initializeTransact(String name, int pcnt, byte[] paramblk, int plen, byte[] datablk, int dlen) + { + + // Set the SMB command code + + if (name == null) + setCommand(Transaction2); + else + setCommand(Transaction); + + // Set the parameter count + + setParameterCount(pcnt); + + // Initialize the parameters + + setParameter(0, plen); // total parameter bytes being sent + setParameter(1, dlen); // total data bytes being sent + + for (int i = 2; i < 9; setParameter(i++, 0)) + ; + + setParameter(6, 1000); // timeout 1 second + setParameter(9, plen); // parameter bytes sent in this packet + setParameter(11, dlen); // data bytes sent in this packet + + setParameter(13, pcnt - STD_PARAMS); // number of setup words + + // Get the data byte offset + + int pos = getByteOffset(); + int startPos = pos; + + // Check if this is a named transaction, if so then store the name + + int idx; + byte[] buf = getBuffer(); + + if (name != null) + { + + // Store the transaction name + + byte[] nam = name.getBytes(); + + for (idx = 0; idx < nam.length; idx++) + buf[pos++] = nam[idx]; + } + + // Word align the buffer offset + + if ((pos % 2) > 0) + pos++; + + // Store the parameter block + + if (paramblk != null) + { + + // Set the parameter block offset + + setParameter(10, pos - m_offset); + + // Store the parameter block + + for (idx = 0; idx < plen; idx++) + buf[pos++] = paramblk[idx]; + } + else + { + + // Clear the parameter block offset + + setParameter(10, 0); + } + + // Word align the data block + + if ((pos % 2) > 0) + pos++; + + // Store the data block + + if (datablk != null) + { + + // Set the data block offset + + setParameter(12, pos - m_offset); + + // Store the data block + + for (idx = 0; idx < dlen; idx++) + buf[pos++] = datablk[idx]; + } + else + { + + // Zero the data block offset + + setParameter(12, 0); + } + + // Set the byte count for the SMB packet + + setByteCount(pos - startPos); + } + + /** + * Set the secondary SMB command + * + * @param cmd Secondary SMB command code. + */ + public final void setAndXCommand(int cmd) + { + m_smbbuf[ANDXCOMMAND + m_offset] = (byte) cmd; + m_smbbuf[ANDXRESERVED + m_offset] = (byte) 0; + } + + /** + * Set the data byte count for this SMB packet + * + * @param cnt Data byte count. + */ + public final void setByteCount(int cnt) + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(cnt, m_smbbuf, offset); + } + + /** + * Set the data byte area in the SMB packet + * + * @param byts Byte array containing the data to be copied to the SMB packet. + */ + public final void setBytes(byte[] byts) + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(byts.length, m_smbbuf, offset); + + offset += 2; + + for (int idx = 0; idx < byts.length; m_smbbuf[offset + idx] = byts[idx++]) + ; + } + + /** + * Set the SMB command + * + * @param cmd SMB command code + */ + public final void setCommand(int cmd) + { + m_smbbuf[COMMAND + m_offset] = (byte) cmd; + } + + /** + * Set the SMB error class. + * + * @param cl SMB error class. + */ + public final void setErrorClass(int cl) + { + m_smbbuf[ERRORCLASS + m_offset] = (byte) (cl & 0xFF); + } + + /** + * Set the SMB error code + * + * @param sts SMB error code. + */ + public final void setErrorCode(int sts) + { + m_smbbuf[ERROR + m_offset] = (byte) (sts & 0xFF); + } + + /** + * Set the SMB flags value. + * + * @param flg SMB flags value. + */ + public final void setFlags(int flg) + { + m_smbbuf[FLAGS + m_offset] = (byte) flg; + } + + /** + * Set the SMB flags2 value. + * + * @param flg SMB flags2 value. + */ + public final void setFlags2(int flg) + { + DataPacker.putIntelShort(flg, m_smbbuf, FLAGS2 + m_offset); + } + + /** + * Set the multiplex identifier. + * + * @param mid Multiplex identifier + */ + public final void setMultiplexId(int mid) + { + DataPacker.putIntelShort(mid, m_smbbuf, MID + m_offset); + } + + /** + * Set the specified parameter word. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + public final void setParameter(int idx, int val) + { + int pos = WORDCNT + (2 * idx) + 1 + m_offset; + DataPacker.putIntelShort(val, m_smbbuf, pos); + } + + /** + * Set the parameter count + * + * @param cnt Parameter word count. + */ + public final void setParameterCount(int cnt) + { + m_smbbuf[WORDCNT + m_offset] = (byte) cnt; + } + + /** + * Set the process identifier value (PID). + * + * @param pid Process identifier value. + */ + public final void setProcessId(int pid) + { + DataPacker.putIntelShort(pid, m_smbbuf, PID + m_offset); + } + + /** + * Set the packet sequence number, for connectionless commands. + * + * @param seq Sequence number. + */ + public final void setSeqNo(int seq) + { + DataPacker.putIntelShort(seq, m_smbbuf, SEQNO + m_offset); + } + + /** + * Set the session id. + * + * @param sid Session id. + */ + public final void setSID(int sid) + { + DataPacker.putIntelShort(sid, m_smbbuf, SID + m_offset); + } + + /** + * Set the tree identifier (TID) + * + * @param tid Tree identifier value. + */ + public final void setTreeId(int tid) + { + DataPacker.putIntelShort(tid, m_smbbuf, TID + m_offset); + } + + /** + * Set the user identifier (UID) + * + * @param uid User identifier value. + */ + public final void setUserId(int uid) + { + DataPacker.putIntelShort(uid, m_smbbuf, UID + m_offset); + } + + /** + * Set the specifiec setup parameter within the SMB packet. + * + * @param idx Setup parameter index. + * @param val Setup parameter value. + */ + public final void setSetupParameter(int idx, int val) + { + setParameter(STD_PARAMS + idx, val); + } + + /** + * Initialize the SMB packet buffer. + */ + private final void initializeBuffer() + { + + // Set the packet signature + + m_smbbuf[SIGNATURE + m_offset] = (byte) 0xFF; + m_smbbuf[SIGNATURE + 1 + m_offset] = (byte) 'S'; + m_smbbuf[SIGNATURE + 2 + m_offset] = (byte) 'M'; + m_smbbuf[SIGNATURE + 3 + m_offset] = (byte) 'B'; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/mailslot/TcpipNetBIOSHostAnnouncer.java b/source/java/org/alfresco/filesys/smb/mailslot/TcpipNetBIOSHostAnnouncer.java new file mode 100644 index 0000000000..7f29cf8075 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/mailslot/TcpipNetBIOSHostAnnouncer.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.mailslot; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.alfresco.filesys.netbios.NetBIOSDatagram; +import org.alfresco.filesys.netbios.NetBIOSDatagramSocket; +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.NetworkSettings; +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; + +/** + *

    + * TCP/IP NetBIOS host announcer implementation. Periodically broadcasts a host announcement + * datagram to inform other Windows networking hosts of the local hosts existence and capabilities. + */ +public class TcpipNetBIOSHostAnnouncer extends HostAnnouncer +{ + + // Default port and announcement interval + + public static final int PORT = RFCNetBIOSProtocol.DATAGRAM; + public static final int INTERVAL = 1; // minutes + + // Local address to bind to, port to use + + private InetAddress m_bindAddress; + private int m_port; + + // Broadcast address and port + + private InetAddress m_bcastAddr; + private int m_bcastPort = RFCNetBIOSProtocol.DATAGRAM; + + // NetBIOS datagram + + private NetBIOSDatagram m_nbdgram; + + /** + * Default constructor. + */ + public TcpipNetBIOSHostAnnouncer() + { + + // Set the default port and interval + + setPort(PORT); + setInterval(INTERVAL); + } + + /** + * Create a host announcer. + * + * @param name Host name to announce + * @param domain Domain name to announce to + * @param intval Announcement interval, in minutes + * @param port Port to use + */ + public TcpipNetBIOSHostAnnouncer(String name, String domain, int intval, int port) + { + + // Add the host to the list of names to announce + + addHostName(name); + setDomain(domain); + setInterval(intval); + + // If port is zero then use the default port + + if (port == 0) + setPort(PORT); + else + setPort(port); + } + + /** + * Get the local address that the announcer should bind to. + * + * @return java.net.InetAddress + */ + public final InetAddress getBindAddress() + { + return m_bindAddress; + } + + /** + * Return the socket/port number that the announcer is using. + * + * @return int + */ + public final int getPort() + { + return m_port; + } + + /** + * Check if the announcer should bind to a particular local address, or all local addresses. + * + * @return boolean + */ + public final boolean hasBindAddress() + { + return m_bindAddress != null ? true : false; + } + + /** + * Set the broadcast address + * + * @param addr String + * @exception UnknownHostException + */ + public final void setBroadcastAddress(String addr) throws UnknownHostException + { + m_bcastAddr = InetAddress.getByName(addr); + } + + /** + * Set the broadcast address and port + * + * @param addr String + * @param int port + * @exception UnknownHostException + */ + public final void setBroadcastAddress(String addr, int port) throws UnknownHostException + { + m_bcastAddr = InetAddress.getByName(addr); + m_bcastPort = port; + } + + /** + * Initialize the host announcer. + * + * @exception Exception + */ + protected void initialize() throws Exception + { + + // Set this thread to be a daemon, set the thread name + + if (hasBindAddress() == false) + setName("TCPHostAnnouncer"); + else + setName("TCPHostAnnouncer_" + getBindAddress().getHostAddress()); + + // Check if at least one host name has been set, if not then use the local host name + + if (numberOfNames() == 0) + { + + // Get the local host name + + addHostName(InetAddress.getLocalHost().getHostName()); + } + + // Allocate the NetBIOS datagram + + m_nbdgram = new NetBIOSDatagram(512); + + // If the broadcast address has not been set, generate a broadcast address + + if (m_bcastAddr == null) + m_bcastAddr = InetAddress.getByName(NetworkSettings.GenerateBroadcastMask(null)); + } + + /** + * Determine if the network connection used for the host announcement is valid + * + * @return boolean + */ + public boolean isNetworkEnabled() + { + return true; + } + + /** + * Send an announcement broadcast. + * + * @param hostName Host name being announced + * @param buf Buffer containing the host announcement mailslot message. + * @param offset Offset to the start of the host announcement message. + * @param len Host announcement message length. + */ + protected void sendAnnouncement(String hostName, byte[] buf, int offset, int len) throws Exception + { + + // Send the host announce datagram + + m_nbdgram.SendDatagram(NetBIOSDatagram.DIRECT_GROUP, hostName, NetBIOSName.FileServer, getDomain(), + NetBIOSName.MasterBrowser, buf, len, offset); + } + + /** + * Set the local address to bind to. + * + * @param addr java.net.InetAddress + */ + public final void setBindAddress(InetAddress addr) + { + m_bindAddress = addr; + NetBIOSDatagramSocket.setBindAddress(addr); + } + + /** + * Set the socket/port number to use. + * + * @param port int + */ + public final void setPort(int port) + { + m_port = port; + NetBIOSDatagramSocket.setDefaultPort(port); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/mailslot/Win32NetBIOSHostAnnouncer.java b/source/java/org/alfresco/filesys/smb/mailslot/Win32NetBIOSHostAnnouncer.java new file mode 100644 index 0000000000..8a681ca48f --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/mailslot/Win32NetBIOSHostAnnouncer.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.mailslot; + +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.win32.NetBIOS; +import org.alfresco.filesys.netbios.win32.Win32NetBIOS; +import org.alfresco.filesys.smb.server.win32.Win32NetBIOSSessionSocketHandler; + +/** + *

    + * The host announcer class periodically broadcasts a host announcement datagram to inform other + * Windows networking hosts of the local hosts existence and capabilities. + *

    + * The Win32 NetBIOS host announcer sends out the announcements using datagrams sent via the Win32 + * Netbios() Netapi32 call. + */ +public class Win32NetBIOSHostAnnouncer extends HostAnnouncer +{ + + // Associated session handler + + Win32NetBIOSSessionSocketHandler m_handler; + + /** + * Create a host announcer. + * + * @param sessHandler Win32NetBIOSSessionSocketHandler + * @param domain Domain name to announce to + * @param intval Announcement interval, in minutes + */ + public Win32NetBIOSHostAnnouncer(Win32NetBIOSSessionSocketHandler handler, String domain, int intval) + { + + // Save the handler + + m_handler = handler; + + // Add the host to the list of names to announce + + addHostName(handler.getServerName()); + setDomain(domain); + setInterval(intval); + } + + /** + * Return the LANA + * + * @return int + */ + public final int getLana() + { + return m_handler.getLANANumber(); + } + + /** + * Return the host name NetBIOS number + * + * @return int + */ + public final int getNameNumber() + { + return m_handler.getNameNumber(); + } + + /** + * Initialize the host announcer. + * + * @exception Exception + */ + protected void initialize() throws Exception + { + + // Set the thread name + + setName("Win32HostAnnouncer_L" + getLana()); + } + + /** + * Determine if the network connection used for the host announcement is valid + * + * @return boolean + */ + public boolean isNetworkEnabled() + { + return m_handler.isLANAValid(); + } + + /** + * Send an announcement broadcast. + * + * @param hostName Host name being announced + * @param buf Buffer containing the host announcement mailslot message. + * @param offset Offset to the start of the host announcement message. + * @param len Host announcement message length. + */ + protected void sendAnnouncement(String hostName, byte[] buf, int offset, int len) throws Exception + { + + // Build the destination NetBIOS name using the domain/workgroup name + + NetBIOSName destNbName = new NetBIOSName(getDomain(), NetBIOSName.MasterBrowser, false); + byte[] destName = destNbName.getNetBIOSName(); + + // Send the host announce datagram via the Win32 Netbios() API call + + int sts = Win32NetBIOS.SendDatagram(getLana(), getNameNumber(), destName, buf, 0, len); + if ( sts != NetBIOS.NRC_GoodRet) + logger.debug("Win32NetBIOS host announce error " + NetBIOS.getErrorString( -sts)); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/mailslot/WinsockNetBIOSHostAnnouncer.java b/source/java/org/alfresco/filesys/smb/mailslot/WinsockNetBIOSHostAnnouncer.java new file mode 100644 index 0000000000..b51028aaa9 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/mailslot/WinsockNetBIOSHostAnnouncer.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.mailslot; + +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.win32.NetBIOS; +import org.alfresco.filesys.netbios.win32.NetBIOSSocket; +import org.alfresco.filesys.netbios.win32.Win32NetBIOS; +import org.alfresco.filesys.smb.server.win32.Win32NetBIOSSessionSocketHandler; + +/** + * Winsock NetBIOS Host Announcer Class + * + *

    + * The host announcer class periodically broadcasts a host announcement datagram to inform other + * Windows networking hosts of the local hosts existence and capabilities. + * + *

    + * The Win32 NetBIOS host announcer sends out the announcements using datagrams sent via Winsock calls. + */ +public class WinsockNetBIOSHostAnnouncer extends HostAnnouncer +{ + // Associated session handler + + private Win32NetBIOSSessionSocketHandler m_handler; + + // Winsock NetBIOS datagram socket + + private NetBIOSSocket m_dgramSocket; + + /** + * Create a host announcer. + * + * @param sessHandler Win32NetBIOSSessionSocketHandler + * @param domain Domain name to announce to + * @param intval Announcement interval, in minutes + */ + public WinsockNetBIOSHostAnnouncer(Win32NetBIOSSessionSocketHandler handler, String domain, int intval) + { + + // Save the handler + + m_handler = handler; + + // Add the host to the list of names to announce + + addHostName(handler.getServerName()); + setDomain(domain); + setInterval(intval); + } + + /** + * Return the LANA + * + * @return int + */ + public final int getLana() + { + return m_handler.getLANANumber(); + } + + /** + * Initialize the host announcer. + * + * @exception Exception + */ + protected void initialize() throws Exception + { + // Set the thread name + + setName("WinsockHostAnnouncer_L" + getLana()); + + // Create the Winsock NetBIOS datagram socket + + m_dgramSocket = NetBIOSSocket.createDatagramSocket(getLana()); + } + + /** + * Determine if the network connection used for the host announcement is valid + * + * @return boolean + */ + public boolean isNetworkEnabled() + { + return m_handler.isLANAValid(); + } + + /** + * Send an announcement broadcast. + * + * @param hostName Host name being announced + * @param buf Buffer containing the host announcement mailslot message. + * @param offset Offset to the start of the host announcement message. + * @param len Host announcement message length. + */ + protected void sendAnnouncement(String hostName, byte[] buf, int offset, int len) throws Exception + { + + // Build the destination NetBIOS name using the domain/workgroup name + + NetBIOSName destNbName = new NetBIOSName(getDomain(), NetBIOSName.MasterBrowser, false); + + // Send the host announce datagram via the Win32 Netbios() API call + + int sts = m_dgramSocket.sendDatagram(destNbName, buf, 0, len); + if ( sts != len) + logger.debug("WinsockNetBIOS host announce error"); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/AdminSharedDevice.java b/source/java/org/alfresco/filesys/smb/server/AdminSharedDevice.java new file mode 100644 index 0000000000..f4e1d24d5b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/AdminSharedDevice.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.server.core.*; + +/** + * Administration shared device, IPC$. + */ +final class AdminSharedDevice extends SharedDevice +{ + + /** + * Class constructor + */ + protected AdminSharedDevice() + { + super("IPC$", ShareType.ADMINPIPE, null); + + // Set the device attributes + + setAttributes(SharedDevice.Admin + SharedDevice.Hidden); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/CoreProtocolHandler.java b/source/java/org/alfresco/filesys/smb/server/CoreProtocolHandler.java new file mode 100644 index 0000000000..d127e3e029 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/CoreProtocolHandler.java @@ -0,0 +1,3779 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.auth.InvalidUserException; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.core.InvalidDeviceInterfaceException; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.filesys.AccessDeniedException; +import org.alfresco.filesys.server.filesys.AccessMode; +import org.alfresco.filesys.server.filesys.DirectoryNotEmptyException; +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.FileAccess; +import org.alfresco.filesys.server.filesys.FileAction; +import org.alfresco.filesys.server.filesys.FileAttribute; +import org.alfresco.filesys.server.filesys.FileExistsException; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.FileSharingException; +import org.alfresco.filesys.server.filesys.FileStatus; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.filesys.server.filesys.SrvDiskInfo; +import org.alfresco.filesys.server.filesys.TooManyConnectionsException; +import org.alfresco.filesys.server.filesys.TooManyFilesException; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.server.filesys.VolumeInfo; +import org.alfresco.filesys.smb.Capability; +import org.alfresco.filesys.smb.DataType; +import org.alfresco.filesys.smb.InvalidUNCPathException; +import org.alfresco.filesys.smb.PCShare; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBDate; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.WildCard; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Core SMB protocol handler class. + */ +class CoreProtocolHandler extends ProtocolHandler +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Special resume ids for '.' and '..' pseudo directories + + private static final int RESUME_START = 0x00008003; + private static final int RESUME_DOT = 0x00008002; + private static final int RESUME_DOTDOT = 0x00008001; + + // Maximum value that can be stored in a parameter word + + private static final int MaxWordValue = 0x0000FFFF; + + // SMB packet class + + protected SMBSrvPacket m_smbPkt; + + /** + * Create a new core SMB protocol handler. + */ + protected CoreProtocolHandler() + { + } + + /** + * Class constructor + * + * @param sess SMBSrvSession + */ + protected CoreProtocolHandler(SMBSrvSession sess) + { + super(sess); + } + + /** + * Return the protocol name + * + * @return String + */ + public String getName() + { + return "Core Protocol"; + } + + /** + * Map a Java exception class to an SMB error code, and return an error response to the caller. + * + * @param ex java.lang.Exception + */ + protected final void MapExceptionToSMBError(Exception ex) + { + + } + + /** + * Pack file information for a search into the specified buffer. + * + * @param buf byte[] Buffer to store data. + * @param bufpos int Position to start storing data. + * @param searchStr Search context string. + * @param resumeId int Resume id + * @param searchId Search context id + * @param info File data to be packed. + * @return int Next available buffer position. + */ + protected final int packSearchInfo(byte[] buf, int bufPos, String searchStr, int resumeId, int searchId, + FileInfo info) + { + + // Pack the resume key + + CoreResumeKey.putResumeKey(buf, bufPos, searchStr, resumeId + (searchId << 16)); + bufPos += CoreResumeKey.LENGTH; + + // Pack the file information + + buf[bufPos++] = (byte) (info.getFileAttributes() & 0x00FF); + + SMBDate dateTime = new SMBDate(info.getModifyDateTime()); + if (dateTime != null) + { + DataPacker.putIntelShort(dateTime.asSMBTime(), buf, bufPos); + DataPacker.putIntelShort(dateTime.asSMBDate(), buf, bufPos + 2); + } + else + { + DataPacker.putIntelShort(0, buf, bufPos); + DataPacker.putIntelShort(0, buf, bufPos + 2); + } + bufPos += 4; + + DataPacker.putIntelInt((int) info.getSize(), buf, bufPos); + bufPos += 4; + + StringBuffer strBuf = new StringBuffer(); + strBuf.append(info.getFileName()); + + while (strBuf.length() < 13) + strBuf.append('\0'); + + if (strBuf.length() > 12) + strBuf.setLength(12); + + DataPacker.putString(strBuf.toString().toUpperCase(), buf, bufPos, true); + bufPos += 13; + + // Return the new buffer position + + return bufPos; + } + + /** + * Check if the specified path exists, and is a directory. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException if an SMB protocol error occurs + */ + protected void procCheckDirectory(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid check directory request + + if (m_smbPkt.checkPacketIsValid(0, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the directory name + + String dirName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (dirName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Directory Check [" + treeId + "] name=" + dirName); + + // Access the disk interface and check for the directory + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Check that the specified path exists, and it is a directory + + if (disk.fileExists(m_sess, conn, dirName) == FileStatus.DirectoryExists) + { + + // The path exists and is a directory, build the valid path response. + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + else + { + + // The path does not exist, or is not a directory. + // + // DOS clients depend on the 'Directory Invalid' (SMB_ERR_BAD_PATH) message being + // returned. + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryInvalid, SMBStatus.ErrDos); + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to delete the directory + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryInvalid, SMBStatus.ErrDos); + return; + } + } + + /** + * Close a file that has been opened on the server. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procCloseFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file close request + + if (m_smbPkt.checkPacketIsValid(3, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + int ftime = m_smbPkt.getParameter(1); + int fdate = m_smbPkt.getParameter(2); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File close [" + treeId + "] fid=" + fid); + + // Close the file + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Close the file + // + // The disk interface may be null if the file is a named pipe file + + if (disk != null) + disk.closeFile(m_sess, conn, netFile); + + // Indicate that the file has been closed + + netFile.setClosed(true); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + } + + // Remove the file from the connections list of open files + + conn.removeFile(fid, getSession()); + + // Build the close file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Create a new directory. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procCreateDirectory(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid create directory request + + if (m_smbPkt.checkPacketIsValid(0, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the directory name + + String dirName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (dirName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Directory Create [" + treeId + "] name=" + dirName); + + // Access the disk interface and create the new directory + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Directory creation parameters + + FileOpenParams params = new FileOpenParams(dirName, FileAction.CreateNotExist, AccessMode.ReadWrite, + FileAttribute.NTDirectory); + + // Create the new directory + + disk.createDirectory(m_sess, conn, params); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (FileExistsException ex) + { + + // Failed to create the directory + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to create directory + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to create the directory + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryInvalid, SMBStatus.ErrDos); + return; + } + + // Build the create directory response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Create a new file on the server. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procCreateFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file create request + + if (m_smbPkt.checkPacketIsValid(3, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the file name + + String fileName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Get the required file attributes for the new file + + int attr = m_smbPkt.getParameter(0); + + // Create the file parameters to be passed to the disk interface + + FileOpenParams params = new FileOpenParams(fileName, FileAction.CreateNotExist, AccessMode.ReadWrite, attr); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Create [" + treeId + "] params=" + params); + + // Access the disk interface and create the new file + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Create the new file + + netFile = disk.createFile(m_sess, conn, params); + + // Add the file to the list of open files for this tree connection + + fid = conn.addFile(netFile, getSession()); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + m_sess.sendErrorResponseSMB(SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + catch (FileExistsException ex) + { + + // File with the requested name already exists + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileAlreadyExists, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the create file response + + outPkt.setParameterCount(1); + outPkt.setParameter(0, fid); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Create a temporary file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procCreateTemporaryFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + } + + /** + * Delete a directory. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procDeleteDirectory(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid delete directory request + + if (m_smbPkt.checkPacketIsValid(0, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the directory name + + String dirName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + + if (dirName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Directory Delete [" + treeId + "] name=" + dirName); + + // Access the disk interface and delete the directory + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Delete the directory + + disk.deleteDirectory(m_sess, conn, dirName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to delete the directory + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (DirectoryNotEmptyException ex) + { + + // Directory not empty + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryNotEmpty, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to delete the directory + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryInvalid, SMBStatus.ErrDos); + return; + } + + // Build the delete directory response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Delete a file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procDeleteFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file delete request + + if (m_smbPkt.checkPacketIsValid(1, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the file name + + String fileName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Delete [" + treeId + "] name=" + fileName); + + // Access the disk interface and delete the file(s) + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Delete file(s) + + disk.deleteFile(m_sess, conn, fileName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the delete file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Get disk attributes processing. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procDiskAttributes(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Get disk attributes"); + + // Parameter and byte count should be zero + + if (m_smbPkt.getParameterCount() != 0 && m_smbPkt.getByteCount() != 0) + { + + // Send an error response + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the disk interface from the shared device + + DiskInterface disk = null; + DiskDeviceContext diskCtx = null; + + try + { + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + diskCtx = (DiskDeviceContext) conn.getContext(); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Create a disk information object and ask the disk interface to fill in the details + + SrvDiskInfo diskInfo = getDiskInformation(disk, diskCtx); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Disk info - total=" + diskInfo.getTotalUnits() + ", free=" + diskInfo.getFreeUnits() + + ", blocksPerUnit=" + diskInfo.getBlocksPerAllocationUnit() + ", blockSize=" + + diskInfo.getBlockSize()); + + // Check if the disk size information needs scaling to fit into 16bit values + + long totUnits = diskInfo.getTotalUnits(); + long freeUnits = diskInfo.getFreeUnits(); + int blocksUnit = diskInfo.getBlocksPerAllocationUnit(); + + while (totUnits > MaxWordValue && blocksUnit <= MaxWordValue) + { + + // Increase the blocks per unit and decrease the total/free units + + blocksUnit *= 2; + + totUnits = totUnits / 2L; + freeUnits = freeUnits / 2L; + } + + // Check if the total/free units fit into a 16bit value + + if (totUnits > MaxWordValue || blocksUnit > MaxWordValue) + { + + // Just use dummy values, cannot fit the disk size into 16bits + + totUnits = MaxWordValue; + + if (freeUnits > MaxWordValue) + freeUnits = MaxWordValue / 2; + + if (blocksUnit > MaxWordValue) + blocksUnit = MaxWordValue; + } + + // Build the reply SMB + + outPkt.setParameterCount(5); + + outPkt.setParameter(0, (int) totUnits); + outPkt.setParameter(1, blocksUnit); + outPkt.setParameter(2, diskInfo.getBlockSize()); + outPkt.setParameter(3, (int) freeUnits); + outPkt.setParameter(4, 0); + + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Echo packet request. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procEcho(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid echo request + + if (m_smbPkt.checkPacketIsValid(1, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the echo count from the request + + int echoCnt = m_smbPkt.getParameter(0); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_ECHO)) + logger.debug("Echo - Count = " + echoCnt); + + // Loop until all echo packets have been sent + + int echoSeq = 1; + + while (echoCnt > 0) + { + + // Set the echo response sequence number + + outPkt.setParameter(0, echoSeq++); + + // Echo the received packet + + m_sess.sendResponseSMB(outPkt); + echoCnt--; + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_ECHO)) + logger.debug("Echo Packet, Seq = " + echoSeq); + } + } + + /** + * Flush the specified file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procFlushFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file flush request + + if (m_smbPkt.checkPacketIsValid(1, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Flush [" + netFile.getFileId() + "]"); + + // Flush the file + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Flush the file + + disk.flushFile(m_sess, conn, netFile); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Flush Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + + // Send the flush response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Get the file attributes for the specified file. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procGetFileAttributes(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid query file information request + + if (m_smbPkt.checkPacketIsValid(0, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the file name + + String fileName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Get File Information [" + treeId + "] name=" + fileName); + + // Access the disk interface and get the file information + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Get the file information for the specified file/directory + + FileInfo finfo = disk.getFileInformation(m_sess, conn, fileName); + if (finfo != null) + { + + // Check if the share is read-only, if so then force the read-only flag for the file + + if (conn.getSharedDevice().isReadOnly() && finfo.isReadOnly() == false) + { + + // Make sure the read-only attribute is set + + finfo.setFileAttributes(finfo.getFileAttributes() + FileAttribute.ReadOnly); + } + + // Return the file information + + outPkt.setParameterCount(10); + outPkt.setParameter(0, finfo.getFileAttributes()); + if (finfo.getModifyDateTime() != 0L) + { + SMBDate dateTime = new SMBDate(finfo.getModifyDateTime()); + outPkt.setParameter(1, dateTime.asSMBTime()); + outPkt.setParameter(2, dateTime.asSMBDate()); + } + else + { + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 0); + } + outPkt.setParameter(3, (int) finfo.getSize() & 0x0000FFFF); + outPkt.setParameter(4, (int) (finfo.getSize() & 0xFFFF0000) >> 16); + + for (int i = 5; i < 10; i++) + outPkt.setParameter(i, 0); + + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + return; + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + } + + // Failed to get the file information + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + } + + /** + * Get file information. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procGetFileInformation(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid query file information2 request + + if (m_smbPkt.checkPacketIsValid(1, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Get File Information 2 [" + netFile.getFileId() + "]"); + + // Access the disk interface and get the file information + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Get the file information for the specified file/directory + + FileInfo finfo = disk.getFileInformation(m_sess, conn, netFile.getFullName()); + if (finfo != null) + { + + // Check if the share is read-only, if so then force the read-only flag for the file + + if (conn.getSharedDevice().isReadOnly() && finfo.isReadOnly() == false) + { + + // Make sure the read-only attribute is set + + finfo.setFileAttributes(finfo.getFileAttributes() + FileAttribute.ReadOnly); + } + + // Initialize the return packet, no data bytes + + outPkt.setParameterCount(11); + outPkt.setByteCount(0); + + // Return the file information + // + // Creation date/time + + SMBDate dateTime = new SMBDate(0); + + if (finfo.getCreationDateTime() != 0L) + { + dateTime.setTime(finfo.getCreationDateTime()); + outPkt.setParameter(0, dateTime.asSMBDate()); + outPkt.setParameter(1, dateTime.asSMBTime()); + } + else + { + outPkt.setParameter(0, 0); + outPkt.setParameter(1, 0); + } + + // Access date/time + + if (finfo.getAccessDateTime() != 0L) + { + dateTime.setTime(finfo.getAccessDateTime()); + outPkt.setParameter(2, dateTime.asSMBDate()); + outPkt.setParameter(3, dateTime.asSMBTime()); + } + else + { + outPkt.setParameter(2, 0); + outPkt.setParameter(3, 0); + } + + // Modify date/time + + if (finfo.getModifyDateTime() != 0L) + { + dateTime.setTime(finfo.getModifyDateTime()); + outPkt.setParameter(4, dateTime.asSMBDate()); + outPkt.setParameter(5, dateTime.asSMBTime()); + } + else + { + outPkt.setParameter(4, 0); + outPkt.setParameter(5, 0); + } + + // File data size + + outPkt.setParameter(6, (int) finfo.getSize() & 0x0000FFFF); + outPkt.setParameter(7, (int) (finfo.getSize() & 0xFFFF0000) >> 16); + + // File allocation size + + outPkt.setParameter(8, (int) finfo.getSize() & 0x0000FFFF); + outPkt.setParameter(9, (int) (finfo.getSize() & 0xFFFF0000) >> 16); + + // File attributes + + outPkt.setParameter(10, finfo.getFileAttributes()); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + return; + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + } + + // Failed to get the file information + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + } + + /** + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procLockFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid lock file request + + if (m_smbPkt.checkPacketIsValid(5, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + long lockcnt = m_smbPkt.getParameterLong(1); + long lockoff = m_smbPkt.getParameterLong(3); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Lock [" + netFile.getFileId() + "] : Offset=" + lockoff + " ,Count=" + lockcnt); + + // ***** Always return a success status, simulated locking **** + // + // Build the lock file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Open a file on the server. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procOpenFile(SMBSrvPacket outPkt) throws IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file open request + + if (m_smbPkt.checkPacketIsValid(2, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the file name + + String fileName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Get the required access mode and the file attributes + + int mode = m_smbPkt.getParameter(0); + int attr = m_smbPkt.getParameter(1); + + // Create the file open parameters to be passed to the disk interface + + FileOpenParams params = new FileOpenParams(fileName, mode, AccessMode.ReadWrite, attr); + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Open [" + treeId + "] params=" + params); + + // Access the disk interface and open the requested file + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Open the requested file + + netFile = disk.openFile(m_sess, conn, params); + + // Add the file to the list of open files for this tree connection + + fid = conn.addFile(netFile, getSession()); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + m_sess.sendErrorResponseSMB(SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // File is not accessible, or file is actually a directory + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (FileSharingException ex) + { + + // Return a sharing violation error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileSharingConflict, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the open file response + + outPkt.setParameterCount(7); + + outPkt.setParameter(0, fid); + outPkt.setParameter(1, 0); // file attributes + + if (netFile.hasModifyDate()) + { + outPkt.setParameterLong(2, (int) (netFile.getModifyDate() / 1000L)); + + // SMBDate smbDate = new SMBDate(netFile.getModifyDate()); + // outPkt.setParameter(2, smbDate.asSMBTime()); // last write time + // outPkt.setParameter(3, smbDate.asSMBDate()); // last write date + } + else + outPkt.setParameterLong(2, 0); + + outPkt.setParameterLong(4, netFile.getFileSizeInt()); // file size + outPkt.setParameter(6, netFile.getGrantedAccess()); + + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process exit, close all open files. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procProcessExit(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid process exit request + + if (m_smbPkt.checkPacketIsValid(0, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Process Exit - Open files = " + conn.openFileCount()); + + // Close all open files + + if (conn.openFileCount() > 0) + { + + // Close all files on the connection + + conn.closeConnection(getSession()); + } + + // Build the process exit response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Read from a file that has been opened on the server. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procReadFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file read request + + if (m_smbPkt.checkPacketIsValid(5, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + int reqcnt = m_smbPkt.getParameter(1); + int reqoff = m_smbPkt.getParameter(2) + (m_smbPkt.getParameter(3) << 16); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Read [" + netFile.getFileId() + "] : Size=" + reqcnt + " ,Pos=" + reqoff); + + // Read data from the file + + byte[] buf = outPkt.getBuffer(); + int rdlen = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Check if the required read size will fit into the reply packet + + int dataOff = outPkt.getByteOffset() + 3; + int availCnt = buf.length - dataOff; + if (m_sess.hasClientCapability(Capability.LargeRead) == false) + availCnt = m_sess.getClientMaximumBufferSize() - dataOff; + + if (availCnt < reqcnt) + { + + // Limit the file read size + + reqcnt = availCnt; + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Read [" + netFile.getFileId() + "] Limited to " + availCnt); + } + + // Read from the file + + rdlen = disk.readFile(m_sess, conn, netFile, buf, outPkt.getByteOffset() + 3, reqcnt, reqoff); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Read Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDReadFault, SMBStatus.ErrHrd); + return; + } + + // Return the data block + + int bytOff = outPkt.getByteOffset(); + buf[bytOff] = (byte) DataType.DataBlock; + DataPacker.putIntelShort(rdlen, buf, bytOff + 1); + outPkt.setByteCount(rdlen + 3); // data type + 16bit length + + outPkt.setParameter(0, rdlen); + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 0); + outPkt.setParameter(3, 0); + outPkt.setParameter(4, 0); + + // Send the read response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Rename a file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procRenameFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid rename file request + + if (m_smbPkt.checkPacketIsValid(1, 4) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the old file name + + boolean isUni = m_smbPkt.isUnicode(); + String oldName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, isUni); + if (oldName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Update the data position + + if (isUni) + { + int len = (oldName.length() * 2) + 2; + dataPos = DataPacker.wordAlign(dataPos + 1) + len; + dataLen -= len; + } + else + { + dataPos += oldName.length() + 2; // string length + null + data type + dataLen -= oldName.length() + 2; + } + + // Extract the new file name + + String newName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, isUni); + if (newName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Rename [" + treeId + "] old name=" + oldName + ", new name=" + newName); + + // Access the disk interface and rename the requested file + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Rename the requested file + + disk.renameFile(m_sess, conn, oldName, newName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the rename file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Start/continue a directory search operation. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procSearch(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid search request + + if (m_smbPkt.checkPacketIsValid(2, 5) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the maximum number of entries to return and the search file attributes + + int maxFiles = m_smbPkt.getParameter(0); + int srchAttr = m_smbPkt.getParameter(1); + + // Check if this is a volume label request + + if ((srchAttr & FileAttribute.Volume) != 0) + { + + // Process the volume label request + + procSearchVolumeLabel(outPkt); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the search file name + + String srchPath = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + + if (srchPath == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidFunc, SMBStatus.ErrDos); + return; + } + + // Update the received data position + + dataPos += srchPath.length() + 2; + dataLen -= srchPath.length() + 2; + + int resumeLen = 0; + + if (buf[dataPos++] == DataType.VariableBlock) + { + + // Extract the resume key length + + resumeLen = DataPacker.getIntelShort(buf, dataPos); + + // Adjust remaining the data length and position + + dataLen -= 3; // block type + resume key length short + dataPos += 2; // resume key length short + + // Check that we received enough data + + if (resumeLen > dataLen) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + } + + // Access the shared devices disk interface + + SearchContext ctx = null; + DiskInterface disk = null; + + try + { + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Check if this is the start of a new search + + byte[] resumeKey = null; + int searchId = -1; + + // Default resume point is at the start of the directory, at the '.' directory if + // directories are + // being returned. + + int resumeId = RESUME_START; + + if (resumeLen == 0 && srchPath.length() > 0) + { + + // Allocate a search slot for the new search + + searchId = m_sess.allocateSearchSlot(); + if (searchId == -1) + { + + // Try and find any 'leaked' searches, ie. searches that have been started but not + // closed. + // + // Windows Explorer seems to leak searches after a new folder has been created, a + // search for '????????.???' + // is started but never continued. + + int idx = 0; + ctx = m_sess.getSearchContext(idx); + + while (ctx != null && searchId == -1) + { + + // Check if the current search context looks like a leaked search. + + if (ctx.getSearchString().compareTo("????????.???") == 0) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Release leaked search [" + idx + "]"); + + // Deallocate the search context + + m_sess.deallocateSearchSlot(idx); + + // Allocate the slot for the new search + + searchId = m_sess.allocateSearchSlot(); + } + else + { + + // Update the search index and get the next search context + + ctx = m_sess.getSearchContext(++idx); + } + } + + // Check if we freed up a search slot + + if (searchId == -1) + { + + // Failed to allocate a slot for the new search + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoResourcesAvailable, SMBStatus.ErrSrv); + return; + } + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Start search [" + searchId + "] - " + srchPath + ", attr=0x" + + Integer.toHexString(srchAttr) + ", maxFiles=" + maxFiles); + + // Start a new search + + ctx = disk.startSearch(m_sess, conn, srchPath, srchAttr); + if (ctx != null) + { + + // Store details of the search in the context + + ctx.setTreeId(treeId); + ctx.setMaximumFiles(maxFiles); + } + + // Save the search context + + m_sess.setSearchContext(searchId, ctx); + } + else + { + + // Take a copy of the resume key + + resumeKey = new byte[CoreResumeKey.LENGTH]; + CoreResumeKey.getResumeKey(buf, dataPos, resumeKey); + + // Get the search context slot id from the resume key, and get the search context. + + int id = CoreResumeKey.getServerArea(resumeKey, 0); + searchId = (id & 0xFFFF0000) >> 16; + ctx = m_sess.getSearchContext(searchId); + + // Check if the search context is valid + + if (ctx == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Get the resume id from the resume key + + resumeId = id & 0x0000FFFF; + + // Restart the search at the resume point, check if the resume point is already set, ie. + // we are just continuing the search. + + if (resumeId < RESUME_DOTDOT && ctx.getResumeId() != resumeId) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search resume at " + resumeId); + + // Restart the search at the specified point + + if (ctx.restartAt(resumeId) == false) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search restart failed"); + + // Failed to restart the search + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + return; + } + } + } + + // Check if the search context is valid + + if (ctx == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Check that the search context and tree connection match + + if (ctx.getTreeId() != treeId) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Start building the search response packet + + outPkt.setParameterCount(1); + int bufPos = outPkt.getByteOffset(); + buf[bufPos] = (byte) DataType.VariableBlock; + bufPos += 3; // save two bytes for the actual block length + int fileCnt = 0; + + // Check if this is the start of a wildcard search and includes directories + + if ((srchAttr & FileAttribute.Directory) != 0 && resumeId >= RESUME_DOTDOT + && WildCard.containsWildcards(srchPath)) + { + + // The first entries in the search should be the '.' and '..' entries for the + // current/parent + // directories. + // + // Remove the file name from the search path, and get the file information for the + // search + // directory. + + String workDir = FileName.removeFileName(srchPath); + FileInfo dirInfo = disk.getFileInformation(m_sess, conn, workDir); + + // Check if we have valid information for the working directory + + if (dirInfo != null) + dirInfo = new FileInfo(".", 0, FileAttribute.Directory); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search adding . and .. entries: " + dirInfo.toString()); + + // Reset the file name to '.' and pack the directory information + + if (resumeId == RESUME_START) + { + + // Pack the '.' file information + + dirInfo.setFileName("."); + resumeId = RESUME_DOT; + bufPos = packSearchInfo(buf, bufPos, ctx.getSearchString(), RESUME_DOT, searchId, dirInfo); + + // Update the file count + + fileCnt++; + } + + // Reset the file name to '..' and pack the directory information + + if (resumeId == RESUME_DOT) + { + + // Pack the '..' file information + + dirInfo.setFileName(".."); + bufPos = packSearchInfo(buf, bufPos, ctx.getSearchString(), RESUME_DOTDOT, searchId, dirInfo); + + // Update the file count + + fileCnt++; + } + } + + // Get files from the search and pack into the return packet + + FileInfo fileInfo = new FileInfo(); + + while (fileCnt < ctx.getMaximumFiles() && ctx.nextFileInfo(fileInfo) == true) + { + + // Check for . files, ignore them. + // + // ** Should check for . and .. file names ** + + if (fileInfo.getFileName().startsWith(".")) + continue; + + // Get the resume id for the current file/directory + + resumeId = ctx.getResumeId(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search return file " + fileInfo.toString() + ", resumeId=" + resumeId); + + // Check if the share is read-only, if so then force the read-only flag for the file + + if (conn.getSharedDevice().isReadOnly() && fileInfo.isReadOnly() == false) + { + + // Make sure the read-only attribute is set + + fileInfo.setFileAttributes(fileInfo.getFileAttributes() + FileAttribute.ReadOnly); + } + + // Pack the file information + + bufPos = packSearchInfo(buf, bufPos, ctx.getSearchString(), resumeId, searchId, fileInfo); + + // Update the file count, reset the current file information + + fileCnt++; + fileInfo.resetInfo(); + } + + // Check if any files were found + + if (fileCnt == 0) + { + + // Send a repsonse that indicates that the search has finished + + outPkt.setParameterCount(1); + outPkt.setParameter(0, 0); + outPkt.setByteCount(0); + + outPkt.setErrorClass(SMBStatus.ErrDos); + outPkt.setErrorCode(SMBStatus.DOSNoMoreFiles); + + m_sess.sendResponseSMB(outPkt); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("End search [" + searchId + "]"); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + } + else + { + + // Set the actual data length + + dataLen = bufPos - outPkt.getByteOffset(); + outPkt.setByteCount(dataLen); + + // Set the variable data block length and returned file count parameter + + bufPos = outPkt.getByteOffset() + 1; + DataPacker.putIntelShort(dataLen - 3, buf, bufPos); + outPkt.setParameter(0, fileCnt); + + // Send the search response packet + + m_sess.sendResponseSMB(outPkt); + + // Check if the search string contains wildcards and this is the start of a new search, + // if not then + // release the search context now as the client will not continue the search. + + if (fileCnt == 1 && resumeLen == 0 && WildCard.containsWildcards(srchPath) == false) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("End search [" + searchId + "] (Not wildcard)"); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + } + } + } + + /** + * Process a search request that is for the volume label. + * + * @param outPkt SMBSrvPacket + */ + protected final void procSearchVolumeLabel(SMBSrvPacket outPkt) throws IOException, SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Start Search - Volume Label"); + + // Access the shared devices disk interface + + DiskInterface disk = null; + DiskDeviceContext diskCtx = null; + + try + { + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + diskCtx = (DiskDeviceContext) conn.getContext(); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Get the volume label + + VolumeInfo volInfo = diskCtx.getVolumeInformation(); + String volLabel = ""; + if (volInfo != null) + volLabel = volInfo.getVolumeLabel(); + + // Start building the search response packet + + outPkt.setParameterCount(1); + int bufPos = outPkt.getByteOffset(); + byte[] buf = outPkt.getBuffer(); + buf[bufPos++] = (byte) DataType.VariableBlock; + + // Calculate the data length + + int dataLen = CoreResumeKey.LENGTH + 22; + DataPacker.putIntelShort(dataLen, buf, bufPos); + bufPos += 2; + + // Pack the resume key + + CoreResumeKey.putResumeKey(buf, bufPos, volLabel, -1); + bufPos += CoreResumeKey.LENGTH; + + // Pack the file information + + buf[bufPos++] = (byte) (FileAttribute.Volume & 0x00FF); + + // Zero the date/time and file length fields + + for (int i = 0; i < 8; i++) + buf[bufPos++] = (byte) 0; + + StringBuffer volBuf = new StringBuffer(); + volBuf.append(volLabel); + + while (volBuf.length() < 13) + volBuf.append(" "); + + if (volBuf.length() > 12) + volBuf.setLength(12); + + bufPos = DataPacker.putString(volBuf.toString().toUpperCase(), buf, bufPos, true); + + // Set the actual data length + + dataLen = bufPos - m_smbPkt.getByteOffset(); + outPkt.setByteCount(dataLen); + + // Send the search response packet + + m_sess.sendResponseSMB(outPkt); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Volume label for " + conn.toString() + " is " + volLabel); + return; + } + + /** + * Seek to the specified file position within the open file. + * + * @param pkt SMBSrvPacket + */ + protected final void procSeekFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file seek request + + if (m_smbPkt.checkPacketIsValid(4, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + int seekMode = m_smbPkt.getParameter(1); + long seekPos = (long) m_smbPkt.getParameterLong(2); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Seek [" + netFile.getFileId() + "] : Mode = " + seekMode + ", Pos = " + seekPos); + + // Seek to the specified position within the file + + byte[] buf = outPkt.getBuffer(); + long pos = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Seek to the file position + + pos = disk.seekFile(m_sess, conn, netFile, seekPos, seekMode); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Seek Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to seek the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDReadFault, SMBStatus.ErrHrd); + return; + } + + // Return the new file position + + outPkt.setParameterCount(2); + outPkt.setParameterLong(0, (int) (pos & 0x0FFFFFFFFL)); + outPkt.setByteCount(0); + + // Send the seek response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the SMB session setup request. + * + * @param outPkt Response SMB packet. + */ + + protected void procSessionSetup(SMBSrvPacket outPkt) throws SMBSrvException, IOException, + TooManyConnectionsException + { + + // Build the session setup response SMB + + outPkt.setParameterCount(3); + outPkt.setParameter(0, 0); + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 8192); + outPkt.setByteCount(0); + + outPkt.setTreeId(0); + outPkt.setUserId(0); + + // Pack the OS, dialect and domain name strings. + + int pos = outPkt.getByteOffset(); + byte[] buf = outPkt.getBuffer(); + + pos = DataPacker.putString("Java", buf, pos, true); + pos = DataPacker.putString("JLAN Server " + m_sess.getServer().isVersion(), buf, pos, true); + pos = DataPacker.putString(m_sess.getServer().getConfiguration().getDomainName(), buf, pos, true); + + outPkt.setByteCount(pos - outPkt.getByteOffset()); + + // Send the negotiate response + + m_sess.sendResponseSMB(outPkt); + + // Update the session state + + m_sess.setState(SMBSrvSessionState.SMBSESSION); + } + + /** + * Set the file attributes for a file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procSetFileAttributes(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid set file attributes request + + if (m_smbPkt.checkPacketIsValid(8, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the file name + + String fileName = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Get the file attributes + + int fattr = m_smbPkt.getParameter(0); + int setFlags = FileInfo.SetAttributes; + + FileInfo finfo = new FileInfo(fileName, 0, fattr); + + int fdate = m_smbPkt.getParameter(1); + int ftime = m_smbPkt.getParameter(2); + + if (fdate != 0 && ftime != 0) + { + finfo.setModifyDateTime(new SMBDate(fdate, ftime).getTime()); + setFlags += FileInfo.SetModifyDate; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Set File Attributes [" + treeId + "] name=" + fileName + ", attr=0x" + + Integer.toHexString(fattr) + ", fdate=" + fdate + ", ftime=" + ftime); + + // Access the disk interface and set the file attributes + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Get the file information for the specified file/directory + + finfo.setFileInformationFlags(setFlags); + disk.setFileInformation(m_sess, conn, fileName, finfo); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + } + + // Return the set file attributes response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Set file information. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procSetFileInformation(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid set file information2 request + + if (m_smbPkt.checkPacketIsValid(7, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request, and get the network file details. + + int fid = m_smbPkt.getParameter(0); + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Get the creation date/time from the request + + int setFlags = 0; + FileInfo finfo = new FileInfo(netFile.getName(), 0, 0); + + int fdate = m_smbPkt.getParameter(1); + int ftime = m_smbPkt.getParameter(2); + + if (fdate != 0 && ftime != 0) + { + finfo.setCreationDateTime(new SMBDate(fdate, ftime).getTime()); + setFlags += FileInfo.SetCreationDate; + } + + // Get the last access date/time from the request + + fdate = m_smbPkt.getParameter(3); + ftime = m_smbPkt.getParameter(4); + + if (fdate != 0 && ftime != 0) + { + finfo.setAccessDateTime(new SMBDate(fdate, ftime).getTime()); + setFlags += FileInfo.SetAccessDate; + } + + // Get the last write date/time from the request + + fdate = m_smbPkt.getParameter(5); + ftime = m_smbPkt.getParameter(6); + + if (fdate != 0 && ftime != 0) + { + finfo.setModifyDateTime(new SMBDate(fdate, ftime).getTime()); + setFlags += FileInfo.SetModifyDate; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Set File Information 2 [" + netFile.getFileId() + "] " + finfo.toString()); + + // Access the disk interface and set the file information + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Get the file information for the specified file/directory + + finfo.setFileInformationFlags(setFlags); + disk.setFileInformation(m_sess, conn, netFile.getFullName(), finfo); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + } + + // Return the set file information response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the SMB tree connect request. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + * @exception TooManyConnectionsException Too many concurrent connections on this session. + */ + + protected void procTreeConnect(SMBSrvPacket outPkt) throws SMBSrvException, TooManyConnectionsException, + java.io.IOException + { + + // Check that the received packet looks like a valid tree connect request + + if (m_smbPkt.checkPacketIsValid(0, 4) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the requested share name, as a UNC path + + boolean isUni = m_smbPkt.isUnicode(); + String uncPath = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, isUni); + if (uncPath == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Extract the password string + + if (isUni) + { + dataPos = DataPacker.wordAlign(dataPos + 1) + (uncPath.length() * 2) + 2; + dataLen -= (uncPath.length() * 2) + 2; + } + else + { + dataPos += uncPath.length() + 2; + dataLen -= uncPath.length() + 2; + } + + String pwd = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, isUni); + if (pwd == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Extract the service type string + + if (isUni) + { + dataPos = DataPacker.wordAlign(dataPos + 1) + (pwd.length() * 2) + 2; + dataLen -= (pwd.length() * 2) + 2; + } + else + { + dataPos += pwd.length() + 2; + dataLen -= pwd.length() + 2; + } + + String service = DataPacker.getDataString(DataType.ASCII, buf, dataPos, dataLen, isUni); + if (service == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Convert the service type to a shared device type + + int servType = ShareType.ServiceAsType(service); + if (servType == ShareType.UNKNOWN) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("Tree connect - " + uncPath + ", " + service); + + // Parse the requested share name + + PCShare share = null; + + try + { + share = new PCShare(uncPath); + } + catch (InvalidUNCPathException ex) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Map the IPC$ share to the admin pipe type + + if (servType == ShareType.NAMEDPIPE && share.getShareName().compareTo("IPC$") == 0) + servType = ShareType.ADMINPIPE; + + // Find the requested shared device + + SharedDevice shareDev = null; + + try + { + + // Get/create the shared device + + shareDev = m_sess.getSMBServer().findShare(share.getNodeName(), share.getShareName(), servType, + getSession(), true); + } + catch (InvalidUserException ex) + { + + // Return a logon failure status + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (Exception ex) + { + + // Return a general status, bad network name + + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidNetworkName, SMBStatus.ErrSrv); + return; + } + + // Check if the share is valid + + if (shareDev == null || shareDev.getType() != servType) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Allocate a tree id for the new connection + + int treeId = m_sess.addConnection(shareDev); + + // Authenticate the share connection depending upon the security mode the server is running + // under + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + int filePerm = FileAccess.Writeable; + + if (auth != null) + { + + // Validate the share connection + + filePerm = auth.authenticateShareConnect(m_sess.getClientInformation(), shareDev, pwd, m_sess); + if (filePerm < 0) + { + + // Invalid share connection request + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + } + + // Set the file permission that this user has been granted for this share + + TreeConnection tree = m_sess.findConnection(treeId); + tree.setPermission(filePerm); + + // Build the tree connect response + + outPkt.setParameterCount(2); + + outPkt.setParameter(0, buf.length - RFCNetBIOSProtocol.HEADER_LEN); + outPkt.setParameter(1, treeId); + outPkt.setByteCount(0); + + // Clear any chained request + + outPkt.setAndXCommand(0xFF); + m_sess.sendResponseSMB(outPkt); + + // Inform the driver that a connection has been opened + + if (tree.getInterface() != null) + tree.getInterface().treeOpened(m_sess, tree); + } + + /** + * Process the SMB tree disconnect request. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procTreeDisconnect(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid tree disconnect request + + if (m_smbPkt.checkPacketIsValid(0, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("Tree disconnect - " + treeId + ", " + conn.toString()); + + // Remove the specified connection from the session + + m_sess.removeConnection(treeId); + + // Build the tree disconnect response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + m_sess.sendResponseSMB(outPkt); + + // Inform the driver that a connection has been closed + + if (conn.getInterface() != null) + conn.getInterface().treeClosed(m_sess, conn); + } + + /** + * Unlock a byte range in the specified file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procUnLockFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid unlock file request + + if (m_smbPkt.checkPacketIsValid(5, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + long lockcnt = m_smbPkt.getParameterLong(1); + long lockoff = m_smbPkt.getParameterLong(3); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File UnLock [" + netFile.getFileId() + "] : Offset=" + lockoff + " ,Count=" + lockcnt); + + // ***** Always return a success status, simulated locking **** + // + // Build the unlock file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Unsupported SMB procesing. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procUnsupported(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Send an unsupported error response + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + + /** + * Write to a file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procWriteFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file write request + + if (m_smbPkt.checkPacketIsValid(5, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + int wrtcnt = m_smbPkt.getParameter(1); + long wrtoff = (m_smbPkt.getParameter(2) + (m_smbPkt.getParameter(3) << 16)) & 0xFFFFFFFFL; + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write [" + netFile.getFileId() + "] : Size=" + wrtcnt + " ,Pos=" + wrtoff); + + // Write data to the file + + byte[] buf = m_smbPkt.getBuffer(); + int pos = m_smbPkt.getByteOffset(); + int wrtlen = 0; + + // Check that the data block is valid + + if (buf[pos] != DataType.DataBlock) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Update the buffer position to the start of the data to be written + + pos += 3; + + // Check for a zero length write, this should truncate/extend the file to the write + // offset position + + if (wrtcnt == 0) + { + + // Truncate/extend the file to the write offset + + disk.truncateFile(m_sess, conn, netFile, wrtoff); + } + else + { + + // Write to the file + + wrtlen = disk.writeFile(m_sess, conn, netFile, buf, pos, wrtcnt, wrtoff); + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + + // Return the count of bytes actually written + + outPkt.setParameterCount(1); + outPkt.setParameter(0, wrtlen); + outPkt.setByteCount(0); + + // Send the write response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Write to a file then close the file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procWriteAndCloseFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file write and close request + + if (m_smbPkt.checkPacketIsValid(6, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + int wrtcnt = m_smbPkt.getParameter(1); + int wrtoff = m_smbPkt.getParameterLong(2); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write And Close [" + netFile.getFileId() + "] : Size=" + wrtcnt + " ,Pos=" + wrtoff); + + // Write data to the file + + byte[] buf = m_smbPkt.getBuffer(); + int pos = m_smbPkt.getByteOffset() + 1; // word align + int wrtlen = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Write to the file + + wrtlen = disk.writeFile(m_sess, conn, netFile, buf, pos, wrtcnt, wrtoff); + + // Close the file + // + // The disk interface may be null if the file is a named pipe file + + if (disk != null) + disk.closeFile(m_sess, conn, netFile); + + // Indicate that the file has been closed + + netFile.setClosed(true); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + + // Return the count of bytes actually written + + outPkt.setParameterCount(1); + outPkt.setParameter(0, wrtlen); + outPkt.setByteCount(0); + + outPkt.setError(0, 0); + + // Send the write response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Run the core SMB protocol handler. + * + * @return boolean true if the packet was processed, else false + */ + public boolean runProtocol() throws java.io.IOException, SMBSrvException, TooManyConnectionsException + { + + // Check if the SMB packet is initialized + + if (m_smbPkt == null) + m_smbPkt = new SMBSrvPacket(m_sess.getBuffer()); + + // Determine the SMB command type + + boolean handledOK = true; + SMBSrvPacket outPkt = m_smbPkt; + + switch (m_smbPkt.getCommand()) + { + + // Session setup + + case PacketType.SessionSetupAndX: + procSessionSetup(outPkt); + break; + + // Tree connect + + case PacketType.TreeConnect: + procTreeConnect(outPkt); + break; + + // Tree disconnect + + case PacketType.TreeDisconnect: + procTreeDisconnect(outPkt); + break; + + // Search + + case PacketType.Search: + procSearch(outPkt); + break; + + // Get disk attributes + + case PacketType.DiskInformation: + procDiskAttributes(outPkt); + break; + + // Get file attributes + + case PacketType.GetFileAttributes: + procGetFileAttributes(outPkt); + break; + + // Set file attributes + + case PacketType.SetFileAttributes: + procSetFileAttributes(outPkt); + break; + + // Get file information + + case PacketType.QueryInformation2: + procGetFileInformation(outPkt); + break; + + // Set file information + + case PacketType.SetInformation2: + procSetFileInformation(outPkt); + break; + + // Open a file + + case PacketType.OpenFile: + procOpenFile(outPkt); + break; + + // Read from a file + + case PacketType.ReadFile: + procReadFile(outPkt); + break; + + // Seek file + + case PacketType.SeekFile: + procSeekFile(outPkt); + break; + + // Close a file + + case PacketType.CloseFile: + procCloseFile(outPkt); + break; + + // Create a new file + + case PacketType.CreateFile: + case PacketType.CreateNew: + procCreateFile(outPkt); + break; + + // Write to a file + + case PacketType.WriteFile: + procWriteFile(outPkt); + break; + + // Write to a file, then close the file + + case PacketType.WriteAndClose: + procWriteAndCloseFile(outPkt); + break; + + // Flush file + + case PacketType.FlushFile: + procFlushFile(outPkt); + break; + + // Rename a file + + case PacketType.RenameFile: + procRenameFile(outPkt); + break; + + // Delete a file + + case PacketType.DeleteFile: + procDeleteFile(outPkt); + break; + + // Create a new directory + + case PacketType.CreateDirectory: + procCreateDirectory(outPkt); + break; + + // Delete a directory + + case PacketType.DeleteDirectory: + procDeleteDirectory(outPkt); + break; + + // Check if a directory exists + + case PacketType.CheckDirectory: + procCheckDirectory(outPkt); + break; + + // Unsupported requests + + case PacketType.IOCtl: + procUnsupported(outPkt); + break; + + // Echo request + + case PacketType.Echo: + procEcho(outPkt); + break; + + // Process exit request + + case PacketType.ProcessExit: + procProcessExit(outPkt); + break; + + // Create temoporary file request + + case PacketType.CreateTemporary: + procCreateTemporaryFile(outPkt); + break; + + // Lock file request + + case PacketType.LockFile: + procLockFile(outPkt); + break; + + // Unlock file request + + case PacketType.UnLockFile: + procUnLockFile(outPkt); + break; + + // Default + + default: + + // Indicate that the protocol handler did not process the SMB request + + handledOK = false; + break; + } + + // Return the handled status + + return handledOK; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/CoreResumeKey.java b/source/java/org/alfresco/filesys/smb/server/CoreResumeKey.java new file mode 100644 index 0000000000..526e353300 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/CoreResumeKey.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.PrintStream; + +import org.alfresco.filesys.util.DataPacker; + +/** + * Core protocol search resume key. + */ +class CoreResumeKey +{ + // Resume key offsets/lengths + + private static final int RESBITS = 0; + private static final int FILENAME = 1; + private static final int RESSERVER = 12; + private static final int RESCONSUMER = 17; + + private static final int FILENAMELEN = 11; + private static final int RESSRVLEN = 5; + private static final int RESCONSUMLEN = 4; + + public static final int LENGTH = 21; + + /** + * Dump the resume key to the specified output stream. + * + * @param out java.io.PrintStream + * @param buf byte[] + * @param pos int + */ + public final static void DumpKey(PrintStream out, byte[] buf, int pos) + { + + // Output the various resume key fields + + out.print("[" + getReservedByte(buf, pos) + ", "); + out.print(getFileName(buf, pos, false) + "]"); + } + + /** + * Return the consumer area of the resume key. + * + * @return byte[] + */ + public static final byte[] getConsumerArea(byte[] buf, int pos) + { + byte[] conArea = new byte[RESCONSUMLEN]; + for (int i = 0; i < RESCONSUMLEN; i++) + conArea[i] = buf[pos + RESCONSUMER + i]; + return conArea; + } + + /** + * Return the file name from the resume key. + * + * @return java.lang.String + */ + public static final String getFileName(byte[] buf, int pos, boolean dot) + { + + // Check if we should return the file name in 8.3 format + + if (dot) + { + + // Build the 8.3 file name + + StringBuffer name = new StringBuffer(); + name.append(new String(buf, pos + FILENAMELEN, 8).trim()); + name.append("."); + name.append(new String(buf, pos + FILENAMELEN + 8, 3).trim()); + + return name.toString(); + } + + // Return the raw string + + return new String(buf, pos + FILENAME, FILENAMELEN).trim(); + } + + /** + * Return the reserved byte from the resume key. + * + * @return byte + */ + public static final byte getReservedByte(byte[] buf, int pos) + { + return buf[pos]; + } + + /** + * Copy the resume key from the buffer to the user buffer. + * + * @param buf byte[] + * @param pos int + * @param key byte[] + */ + public final static void getResumeKey(byte[] buf, int pos, byte[] key) + { + + // Copy the resume key bytes + + System.arraycopy(buf, pos, key, 0, LENGTH); + } + + /** + * Return the server area resume key value. This is the search context index in our case. + * + * @return int Server resume key value ( search context index). + */ + public static final int getServerArea(byte[] buf, int pos) + { + return DataPacker.getIntelInt(buf, pos + RESSERVER + 1); + } + + /** + * Generate a resume key with the specified filename and search context id. + * + * @param buf byte[] + * @param pos + * @param fileName java.lang.String + * @param ctxId int + */ + public final static void putResumeKey(byte[] buf, int pos, String fileName, int ctxId) + { + + // Clear the reserved area + + buf[pos + RESBITS] = 0x16; + + // Put the file name in resume key format + + setFileName(buf, pos, fileName); + + // Put the server side reserved area + + setServerArea(buf, pos, ctxId); + // setServerArea( buf, pos, 0); + } + + /** + * Set the consumer reserved area value. + * + * @param conArea byte[] + */ + public static final void setConsumerArea(byte[] buf, int pos, byte[] conArea) + { + for (int i = 0; i < RESCONSUMLEN; i++) + buf[pos + RESCONSUMER + i] = conArea[i]; + } + + /** + * Set the resume key file name string. + * + * @param name java.lang.String + */ + public static final void setFileName(byte[] buf, int pos, String name) + { + + // Split the file name string + + StringBuffer str = new StringBuffer(); + int dot = name.indexOf("."); + if (dot != -1) + { + str.append(name.substring(0, dot)); + while (str.length() < 8) + str.append(" "); + str.append(name.substring(dot + 1, name.length())); + } + else + str.append(name); + + // Space fill the file name to 11 characters + + while (str.length() < FILENAMELEN) + str.append(" "); + + // Pack the file name string into the resume key + + DataPacker.putString(str.toString(), buf, pos + FILENAME, false); + } + + /** + * Set the resume key reserved byte value. + * + * @param param byte + */ + public static final void setReservedByte(byte[] buf, int pos, byte val) + { + buf[pos] = val; + } + + /** + * Set the resume key server area value. This is the search context index in our case. + * + * @param srvVal int + */ + public static final void setServerArea(byte[] buf, int pos, int srvVal) + { + buf[pos + RESSERVER] = 1; + DataPacker.putIntelInt(srvVal, buf, pos + RESSERVER + 1); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/DCERPCHandler.java b/source/java/org/alfresco/filesys/smb/server/DCERPCHandler.java new file mode 100644 index 0000000000..fd362facc9 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/DCERPCHandler.java @@ -0,0 +1,799 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.smb.DataType; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.TransactBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBuffer; +import org.alfresco.filesys.smb.dcerpc.DCEBufferException; +import org.alfresco.filesys.smb.dcerpc.DCECommand; +import org.alfresco.filesys.smb.dcerpc.DCEDataPacker; +import org.alfresco.filesys.smb.dcerpc.DCEPipeType; +import org.alfresco.filesys.smb.dcerpc.UUID; +import org.alfresco.filesys.smb.dcerpc.server.DCEPipeFile; +import org.alfresco.filesys.smb.dcerpc.server.DCESrvPacket; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * DCE/RPC Protocol Handler Class + */ +public class DCERPCHandler +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + /** + * Process a DCE/RPC request + * + * @param sess SMBSrvSession + * @param srvTrans SMBSrvTransPacket + * @param outPkt SMBSrvPacket + * @exception IOException + * @exception SMBSrvException + */ + public static final void processDCERPCRequest(SMBSrvSession sess, SMBSrvTransPacket srvTrans, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = srvTrans.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Get the file id and validate + + int fid = srvTrans.getSetupParameter(1); + int maxData = srvTrans.getParameter(3) - DCEBuffer.OPERATIONDATA; + + // Get the IPC pipe file for the specified file id + + DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid); + if (pipeFile == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Create a DCE/RPC buffer from the received data + + DCEBuffer dceBuf = new DCEBuffer(srvTrans.getBuffer(), srvTrans.getParameter(10) + + RFCNetBIOSProtocol.HEADER_LEN); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("TransactNmPipe pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", dceCmd=0x" + + Integer.toHexString(dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE))); + + // Process the received DCE buffer + + processDCEBuffer(sess, dceBuf, pipeFile); + + // Check if there is a reply buffer to return to the caller + + if (pipeFile.hasBufferedData() == false) + return; + + DCEBuffer txBuf = pipeFile.getBufferedData(); + + // Initialize the reply + + DCESrvPacket dcePkt = new DCESrvPacket(outPkt.getBuffer()); + + // Always only one fragment as the data either fits into the first reply fragment or the + // client will read the remaining data by issuing read requests on the pipe + + int flags = DCESrvPacket.FLG_ONLYFRAG; + + dcePkt.initializeDCEReply(); + txBuf.setHeaderValue(DCEBuffer.HDR_FLAGS, flags); + + // Build the reply data + + byte[] buf = dcePkt.getBuffer(); + int pos = DCEDataPacker.longwordAlign(dcePkt.getByteOffset()); + + // Set the DCE fragment size and send the reply DCE/RPC SMB + + int dataLen = txBuf.getLength(); + txBuf.setHeaderValue(DCEBuffer.HDR_FRAGLEN, dataLen); + + // Copy the data from the DCE output buffer to the reply SMB packet + + int len = txBuf.getLength(); + int sts = SMBStatus.NTSuccess; + + if (len > maxData) + { + + // Write the maximum transmit fragment to the reply + + len = maxData + DCEBuffer.OPERATIONDATA; + dataLen = maxData + DCEBuffer.OPERATIONDATA; + + // Indicate a buffer overflow status + + sts = SMBStatus.NTBufferOverflow; + } + else + { + + // Clear the DCE/RPC pipe buffered data, the reply will fit into a single response + // packet + + pipeFile.setBufferedData(null); + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("Reply DCEbuf flags=0x" + Integer.toHexString(flags) + ", len=" + len + ", status=0x" + + Integer.toHexString(sts)); + + // Copy the reply data to the reply packet + + try + { + pos += txBuf.copyData(buf, pos, len); + } + catch (DCEBufferException ex) + { + sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Set the SMB transaction data length + + int byteLen = pos - dcePkt.getByteOffset(); + dcePkt.setParameter(1, dataLen); + dcePkt.setParameter(6, dataLen); + dcePkt.setByteCount(byteLen); + dcePkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE); + dcePkt.setLongErrorCode(sts); + + sess.sendResponseSMB(dcePkt); + } + + /** + * Process a DCE/RPC request + * + * @param sess SMBSrvSession + * @param tbuf TransactBuffer + * @param outPkt SMBSrvPacket + * @exception IOException + * @exception SMBSrvException + */ + public static final void processDCERPCRequest(SMBSrvSession sess, TransactBuffer tbuf, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Check if the transaction buffer has setup and data buffers + + if (tbuf.hasSetupBuffer() == false || tbuf.hasDataBuffer() == false) + { + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = tbuf.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Get the file id and validate + + DataBuffer setupBuf = tbuf.getSetupBuffer(); + + setupBuf.skipBytes(2); + int fid = setupBuf.getShort(); + int maxData = tbuf.getReturnDataLimit() - DCEBuffer.OPERATIONDATA; + + // Get the IPC pipe file for the specified file id + + DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid); + if (pipeFile == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Create a DCE/RPC buffer from the received transaction data + + DCEBuffer dceBuf = new DCEBuffer(tbuf); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("TransactNmPipe pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", dceCmd=0x" + + Integer.toHexString(dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE))); + + // Process the received DCE buffer + + processDCEBuffer(sess, dceBuf, pipeFile); + + // Check if there is a reply buffer to return to the caller + + if (pipeFile.hasBufferedData() == false) + return; + + DCEBuffer txBuf = pipeFile.getBufferedData(); + + // Initialize the reply + + DCESrvPacket dcePkt = new DCESrvPacket(outPkt.getBuffer()); + + // Always only one fragment as the data either fits into the first reply fragment or the + // client will read the remaining data by issuing read requests on the pipe + + int flags = DCESrvPacket.FLG_ONLYFRAG; + + dcePkt.initializeDCEReply(); + txBuf.setHeaderValue(DCEBuffer.HDR_FLAGS, flags); + + // Build the reply data + + byte[] buf = dcePkt.getBuffer(); + int pos = DCEDataPacker.longwordAlign(dcePkt.getByteOffset()); + + // Set the DCE fragment size and send the reply DCE/RPC SMB + + int dataLen = txBuf.getLength(); + txBuf.setHeaderValue(DCEBuffer.HDR_FRAGLEN, dataLen); + + // Copy the data from the DCE output buffer to the reply SMB packet + + int len = txBuf.getLength(); + int sts = SMBStatus.NTSuccess; + + if (len > maxData) + { + + // Write the maximum transmit fragment to the reply + + len = maxData + DCEBuffer.OPERATIONDATA; + dataLen = maxData + DCEBuffer.OPERATIONDATA; + + // Indicate a buffer overflow status + + sts = SMBStatus.NTBufferOverflow; + } + else + { + + // Clear the DCE/RPC pipe buffered data, the reply will fit into a single response + // packet + + pipeFile.setBufferedData(null); + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("Reply DCEbuf flags=0x" + Integer.toHexString(flags) + ", len=" + len + ", status=0x" + + Integer.toHexString(sts)); + + // Copy the reply data to the reply packet + + try + { + pos += txBuf.copyData(buf, pos, len); + } + catch (DCEBufferException ex) + { + sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Set the SMB transaction data length + + int byteLen = pos - dcePkt.getByteOffset(); + dcePkt.setParameter(1, dataLen); + dcePkt.setParameter(6, dataLen); + dcePkt.setByteCount(byteLen); + dcePkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE); + dcePkt.setLongErrorCode(sts); + + sess.sendResponseSMB(dcePkt); + } + + /** + * Process a DCE/RPC write request to the named pipe file + * + * @param sess SMBSrvSession + * @param inPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + * @exception IOException + * @exception SMBSrvException + */ + public static final void processDCERPCRequest(SMBSrvSession sess, SMBSrvPacket inPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = inPkt.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Determine if this is a write or write andX request + + int cmd = inPkt.getCommand(); + + // Get the file id and validate + + int fid = -1; + if (cmd == PacketType.WriteFile) + fid = inPkt.getParameter(0); + else + fid = inPkt.getParameter(2); + + // Get the IPC pipe file for the specified file id + + DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid); + if (pipeFile == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Create a DCE buffer for the received data + + DCEBuffer dceBuf = null; + byte[] buf = inPkt.getBuffer(); + int pos = 0; + int len = 0; + + if (cmd == PacketType.WriteFile) + { + + // Get the data offset + + pos = inPkt.getByteOffset(); + + // Check that the received data is valid + + if (buf[pos++] != DataType.DataBlock) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + len = DataPacker.getIntelShort(buf, pos); + pos += 2; + + } + else + { + + // Get the data offset and length + + len = inPkt.getParameter(10); + pos = inPkt.getParameter(11) + RFCNetBIOSProtocol.HEADER_LEN; + } + + // Create a DCE buffer mapped to the received packet + + dceBuf = new DCEBuffer(buf, pos); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("Write pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", dceCmd=0x" + + Integer.toHexString(dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE))); + + // Process the DCE buffer + + processDCEBuffer(sess, dceBuf, pipeFile); + + // Check if there is a valid reply buffered + + int bufLen = 0; + if (pipeFile.hasBufferedData()) + bufLen = pipeFile.getBufferedData().getLength(); + + // Send the write/write andX reply + + if (cmd == PacketType.WriteFile) + { + + // Build the write file reply + + outPkt.setParameterCount(1); + outPkt.setParameter(0, len); + outPkt.setByteCount(0); + } + else + { + + // Build the write andX reply + + outPkt.setParameterCount(6); + + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); + outPkt.setParameter(2, len); + outPkt.setParameter(3, bufLen); + outPkt.setParameter(4, 0); + outPkt.setParameter(5, 0); + outPkt.setByteCount(0); + } + + // Send the write reply + + outPkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE); + sess.sendResponseSMB(outPkt); + } + + /** + * Process a DCE/RPC pipe read request + * + * @param sess SMBSrvSession + * @param inPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + * @exception IOException + * @exception SMBSrvException + */ + public static final void processDCERPCRead(SMBSrvSession sess, SMBSrvPacket inPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = inPkt.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Determine if this is a read or read andX request + + int cmd = inPkt.getCommand(); + + // Get the file id and read length, and validate + + int fid = -1; + int rdLen = -1; + + if (cmd == PacketType.ReadFile) + { + fid = inPkt.getParameter(0); + rdLen = inPkt.getParameter(1); + } + else + { + fid = inPkt.getParameter(2); + rdLen = inPkt.getParameter(5); + } + + // Get the IPC pipe file for the specified file id + + DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid); + if (pipeFile == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("Read pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", rdLen=" + rdLen); + + // Check if there is a valid reply buffered + + if (pipeFile.hasBufferedData()) + { + + // Get the buffered data + + DCEBuffer bufData = pipeFile.getBufferedData(); + int bufLen = bufData.getAvailableLength(); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug(" Buffered data available=" + bufLen); + + // Check if there is less data than the read size + + if (rdLen > bufLen) + rdLen = bufLen; + + // Build the read response + + if (cmd == PacketType.ReadFile) + { + + // Build the read response + + outPkt.setParameterCount(5); + outPkt.setParameter(0, rdLen); + for (int i = 1; i < 5; i++) + outPkt.setParameter(i, 0); + outPkt.setByteCount(rdLen + 3); + + // Copy the data to the response + + byte[] buf = outPkt.getBuffer(); + int pos = outPkt.getByteOffset(); + + buf[pos++] = (byte) DataType.DataBlock; + DataPacker.putIntelShort(rdLen, buf, pos); + pos += 2; + + try + { + bufData.copyData(buf, pos, rdLen); + } + catch (DCEBufferException ex) + { + logger.error("DCR/RPC read", ex); + } + } + else + { + + // Build the read andX response + + outPkt.setParameterCount(12); + outPkt.setAndXCommand(0xFF); + for (int i = 1; i < 12; i++) + outPkt.setParameter(i, 0); + + // Copy the data to the response + + byte[] buf = outPkt.getBuffer(); + int pos = DCEDataPacker.longwordAlign(outPkt.getByteOffset()); + + outPkt.setParameter(5, rdLen); + outPkt.setParameter(6, pos - RFCNetBIOSProtocol.HEADER_LEN); + outPkt.setByteCount((pos + rdLen) - outPkt.getByteOffset()); + + try + { + bufData.copyData(buf, pos, rdLen); + } + catch (DCEBufferException ex) + { + logger.error("DCE/RPC error", ex); + } + } + } + else + { + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug(" No buffered data available"); + + // Return a zero length read response + + if (cmd == PacketType.ReadFile) + { + + // Initialize the read response + + outPkt.setParameterCount(5); + for (int i = 0; i < 5; i++) + outPkt.setParameter(i, 0); + outPkt.setByteCount(0); + } + else + { + + // Return a zero length read andX response + + outPkt.setParameterCount(12); + + outPkt.setAndXCommand(0xFF); + for (int i = 1; i < 12; i++) + outPkt.setParameter(i, 0); + outPkt.setByteCount(0); + } + } + + // Clear the status code + + outPkt.setLongErrorCode(SMBStatus.NTSuccess); + + // Send the read reply + + outPkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE); + sess.sendResponseSMB(outPkt); + } + + /** + * Process the DCE/RPC request buffer + * + * @param sess SMBSrvSession + * @param buf DCEBuffer + * @param pipeFile DCEPipeFile + * @exception IOException + * @exception SMBSrvException + */ + public static final void processDCEBuffer(SMBSrvSession sess, DCEBuffer dceBuf, DCEPipeFile pipeFile) + throws IOException, SMBSrvException + { + + // Process the DCE/RPC request + + switch (dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE)) + { + + // DCE Bind + + case DCECommand.BIND: + procDCEBind(sess, dceBuf, pipeFile); + break; + + // DCE Request + + case DCECommand.REQUEST: + procDCERequest(sess, dceBuf, pipeFile); + break; + + default: + sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + break; + } + } + + /** + * Process a DCE bind request + * + * @param sess SMBSrvSession + * @param dceBuf DCEBuffer + * @param pipeFile DCEPipeFile + * @exception IOException + * @exception SMBSrvException + */ + public static final void procDCEBind(SMBSrvSession sess, DCEBuffer dceBuf, DCEPipeFile pipeFile) + throws IOException, SMBSrvException + { + + try + { + + // DEBUG + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("DCE Bind"); + + // Get the call id and skip the DCE header + + int callId = dceBuf.getHeaderValue(DCEBuffer.HDR_CALLID); + dceBuf.skipBytes(DCEBuffer.DCEDATA); + + // Unpack the bind request + + int maxTxSize = dceBuf.getShort(); + int maxRxSize = dceBuf.getShort(); + int groupId = dceBuf.getInt(); + int ctxElems = dceBuf.getByte(DCEBuffer.ALIGN_INT); + int presCtxId = dceBuf.getByte(DCEBuffer.ALIGN_SHORT); + int trfSyntax = dceBuf.getByte(DCEBuffer.ALIGN_SHORT); + + UUID uuid1 = dceBuf.getUUID(true); + UUID uuid2 = dceBuf.getUUID(true); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + { + logger.debug("Bind: maxTx=" + maxTxSize + ", maxRx=" + maxRxSize + ", groupId=" + groupId + + ", ctxElems=" + ctxElems + ", presCtxId=" + presCtxId + ", trfSyntax=" + trfSyntax); + logger.debug(" uuid1=" + uuid1.toString()); + logger.debug(" uuid2=" + uuid2.toString()); + } + + // Update the IPC pipe file + + pipeFile.setMaxTransmitFragmentSize(maxTxSize); + pipeFile.setMaxReceiveFragmentSize(maxRxSize); + + // Create an output DCE buffer for the reply and add the bind acknowledge header + + DCEBuffer txBuf = new DCEBuffer(); + txBuf.putBindAckHeader(dceBuf.getHeaderValue(DCEBuffer.HDR_CALLID)); + txBuf.setHeaderValue(DCEBuffer.HDR_FLAGS, DCEBuffer.FLG_ONLYFRAG); + + // Pack the bind acknowledge DCE reply + + txBuf.putShort(maxTxSize); + txBuf.putShort(maxRxSize); + txBuf.putInt(0x53F0); + + String srvPipeName = DCEPipeType.getServerPipeName(pipeFile.getPipeId()); + txBuf.putShort(srvPipeName.length() + 1); + txBuf.putASCIIString(srvPipeName, true, DCEBuffer.ALIGN_INT); + txBuf.putInt(1); + txBuf.putShort(0); + txBuf.putShort(0); + txBuf.putUUID(uuid2, true); + + txBuf.setHeaderValue(DCEBuffer.HDR_FRAGLEN, txBuf.getLength()); + + // Attach the reply buffer to the pipe file + + pipeFile.setBufferedData(txBuf); + } + catch (DCEBufferException ex) + { + sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + } + + /** + * Process a DCE request + * + * @param sess SMBSrvSession + * @param dceBuf DCEBuffer + * @param pipeFile DCEPipeFile + * @exception IOException + * @exception SMBSrvException + */ + public static final void procDCERequest(SMBSrvSession sess, DCEBuffer inBuf, DCEPipeFile pipeFile) + throws IOException, SMBSrvException + { + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC)) + logger.debug("DCE Request opNum=0x" + Integer.toHexString(inBuf.getHeaderValue(DCEBuffer.HDR_OPCODE))); + + // Pass the request to the DCE pipe request handler + + if (pipeFile.hasRequestHandler()) + pipeFile.getRequestHandler().processRequest(sess, inBuf, pipeFile); + else + sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/DiskInfoPacker.java b/source/java/org/alfresco/filesys/smb/server/DiskInfoPacker.java new file mode 100644 index 0000000000..62f4924610 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/DiskInfoPacker.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.server.filesys.DiskInfo; +import org.alfresco.filesys.server.filesys.SrvDiskInfo; +import org.alfresco.filesys.server.filesys.VolumeInfo; +import org.alfresco.filesys.smb.NTTime; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; + +/** + * Disk information packer class. + */ +class DiskInfoPacker +{ + + // Disk information levels + + public static final int InfoStandard = 1; + public static final int InfoVolume = 2; + public static final int InfoFsVolume = 0x102; + public static final int InfoFsSize = 0x103; + public static final int InfoFsDevice = 0x104; + public static final int InfoFsAttribute = 0x105; + public static final int InfoCifsUnix = 0x200; + public static final int InfoMacFsInfo = 0x301; + public static final int InfoFullFsSize = 0x3EF; + + // Mac support flags + + public static final int MacAccessControl = 0x0010; + public static final int MacGetSetComments = 0x0020; + public static final int MacDesktopDbCalls = 0x0040; + public static final int MacUniqueIds = 0x0080; + public static final int MacNoStreamsOrMacSupport = 0x0100; + + /** + * Class constructor. + */ + public DiskInfoPacker() + { + super(); + } + + /** + * Pack the standard disk information, InfoStandard. + * + * @param info SMBDiskInfo to be packed. + * @param buf Buffer to pack the data into. + */ + public final static void packStandardInfo(DiskInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG File system identifier, always 0 ? + // ULONG Sectors per allocation unit. + // ULONG Total allocation units. + // ULONG Total available allocation units. + // USHORT Number of bytes per sector. + + // Pack the file system identifier, 0 = NT file system + + buf.putZeros(4); + // buf.putInt(999); + + // Pack the disk unit information + + buf.putInt(info.getBlocksPerAllocationUnit()); + buf.putInt((int) info.getTotalUnits()); + buf.putInt((int) info.getFreeUnits()); + buf.putShort(info.getBlockSize()); + } + + /** + * Pack the volume label information, InfoVolume. + * + * @param info Volume information + * @param buf Buffer to pack data into. + * @param uni Use Unicode strings if true, else use ASCII strings + */ + public final static void packVolumeInfo(VolumeInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG Volume serial number + // UCHAR Volume label length + // STRING Volume label + + // Pack the volume serial number + + buf.putInt(info.getSerialNumber()); + + // Pack the volume label length and string + + buf.putByte(info.getVolumeLabel().length()); + buf.putString(info.getVolumeLabel(), uni); + } + + /** + * Pack the filesystem size information, InfoFsSize + * + * @param info Disk size information + * @param buf Buffer to pack data into. + */ + public final static void packFsSizeInformation(SrvDiskInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG Disk size (in units) + // ULONG Free size (in units) + // UINT Unit size in blocks + // UINT Block size in bytes + + buf.putLong(info.getTotalUnits()); + buf.putLong(info.getFreeUnits()); + buf.putInt(info.getBlocksPerAllocationUnit()); + buf.putInt(info.getBlockSize()); + } + + /** + * Pack the filesystem volume information, InfoFsVolume + * + * @param info Volume information + * @param buf Buffer to pack data into. + * @param uni Use Unicode strings if true, else use ASCII strings + */ + public final static void packFsVolumeInformation(VolumeInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG Volume creation date/time (NT 64bit time fomat) + // UINT Volume serial number + // UINT Volume label length + // SHORT Reserved + // STRING Volume label (no null) + + if (info.hasCreationDateTime()) + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + else + buf.putZeros(8); + + if (info.hasSerialNumber()) + buf.putInt(info.getSerialNumber()); + else + buf.putZeros(4); + + int len = info.getVolumeLabel().length(); + if (uni) + len *= 2; + buf.putInt(len); + + buf.putZeros(2); // reserved + buf.putString(info.getVolumeLabel(), uni, false); + } + + /** + * Pack the filesystem device information, InfoFsDevice + * + * @param typ Device type + * @param devChar Device characteristics + * @param buf Buffer to pack data into. + */ + public final static void packFsDevice(int typ, int devChar, DataBuffer buf) + { + + // Information format :- + // UINT Device type + // UINT Characteristics + + buf.putInt(typ); + buf.putInt(devChar); + } + + /** + * Pack the filesystem attribute information, InfoFsAttribute + * + * @param attr Attribute flags + * @param maxName Maximum file name component length + * @param fsType File system type name + * @param uni Unicode strings required + * @param buf Buffer to pack data into. + */ + public final static void packFsAttribute(int attr, int maxName, String fsType, boolean uni, DataBuffer buf) + { + + // Information format :- + // UINT Attribute flags + // UINT Maximum filename component length (usually 255) + // UINT Filesystem type length + // STRING Filesystem type string + + buf.putInt(attr); + buf.putInt(maxName); + + if (uni) + buf.putInt(fsType.length() * 2); + else + buf.putInt(fsType.length()); + buf.putString(fsType, uni, false); + } + + /** + * Pack the Mac filesystem information, InfoMacFsInfo + * + * @param diskInfo SMBDiskInfo to be packed. + * @param volInfo Volume information to be packed + * @param ntfs Filesystem supports NTFS streams + * @param buf Buffer to pack the data into. + */ + public final static void packMacFsInformation(DiskInfo diskInfo, VolumeInfo volInfo, boolean ntfs, DataBuffer buf) + { + + // Information format :- + // LARGE_INTEGER Volume creation time (NT format) + // LARGE_INTEGER Volume modify time (NT format) + // LARGE_INTEGER Volume backup time (NT format) + // ULONG Allocation blocks + // ULONG Allocation block size (multiple of 512) + // ULONG Free blocks on the volume + // UCHAR[32] Finder info + // LONG Number of files in root directory (zero if unknown) + // LONG Number of directories in the root directory (zero if unknown) + // LONG Number of files on the volume (zero if unknown) + // LONG Number of directories on the volume (zero if unknown) + // LONG Mac support flags (big endian) + + // Pack the volume creation time + + if (volInfo.hasCreationDateTime()) + { + long ntTime = NTTime.toNTTime(volInfo.getCreationDateTime()); + buf.putLong(ntTime); + buf.putLong(ntTime); + buf.putLong(ntTime); + } + else + buf.putZeros(24); + + // Pack the number of allocation blocks, block size and free block count + + buf.putInt((int) diskInfo.getTotalUnits()); + buf.putInt(diskInfo.getBlockSize() * diskInfo.getBlocksPerAllocationUnit()); + buf.putInt((int) diskInfo.getFreeUnits()); + + // Pack the finder information area + + buf.putZeros(32); + + // Pack the file/directory counts + + buf.putInt(0); + buf.putInt(0); + buf.putInt(0); + buf.putInt(0); + + // Pack the Mac support flags + + DataPacker.putIntelInt(ntfs ? 0 : MacNoStreamsOrMacSupport, buf.getBuffer(), buf.getPosition()); + buf.setPosition(buf.getPosition() + 4); + } + + /** + * Pack the filesystem size information, InfoFsSize + * + * @param userLimit User free units + * @param info Disk size information + * @param buf Buffer to pack data into. + */ + public final static void packFullFsSizeInformation(long userLimit, SrvDiskInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG Disk size (in units) + // ULONG User free size (in units) + // ULONG Free size (in units) + // UINT Unit size in blocks + // UINT Block size in bytes + + buf.putLong(info.getTotalUnits()); + buf.putLong(userLimit); + buf.putLong(info.getFreeUnits()); + buf.putInt(info.getBlocksPerAllocationUnit()); + buf.putInt(info.getBlockSize()); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/Find.java b/source/java/org/alfresco/filesys/smb/server/Find.java new file mode 100644 index 0000000000..3643cf1e6c --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/Find.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +/** + * Find First Flags Class + */ +class Find +{ + // Find first flags + + protected static final int CloseSearch = 0x01; + protected static final int CloseSearchAtEnd = 0x02; + protected static final int ResumeKeysRequired = 0x04; + protected static final int ContinuePrevious = 0x08; + protected static final int BackupIntent = 0x10; +} diff --git a/source/java/org/alfresco/filesys/smb/server/FindInfoPacker.java b/source/java/org/alfresco/filesys/smb/server/FindInfoPacker.java new file mode 100644 index 0000000000..eb0045f03e --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/FindInfoPacker.java @@ -0,0 +1,945 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.UnsupportedInfoLevelException; +import org.alfresco.filesys.smb.NTTime; +import org.alfresco.filesys.smb.SMBDate; +import org.alfresco.filesys.util.DataBuffer; + +/** + * Find Information Packer Class + *

    + * Pack file information for a find first/find next information level. + */ +class FindInfoPacker +{ + + // Enable 8.3 name generation (required for Mac OS9) + + private static final boolean Enable8Dot3Names = false; + + // Enable packing of file id + + private static final boolean EnableFileIdPacking = false; + + // File information levels + + public static final int InfoStandard = 1; + public static final int InfoQueryEASize = 2; + public static final int InfoQueryEAFromList = 3; + public static final int InfoDirectory = 0x101; + public static final int InfoFullDirectory = 0x102; + public static final int InfoNames = 0x103; + public static final int InfoDirectoryBoth = 0x104; + public static final int InfoMacHfsInfo = 0x302; + + // File information fixed lengths, includes nulls on strings. + + public static final int InfoStandardLen = 24; + public static final int InfoQueryEASizeLen = 28; + public static final int InfoDirectoryLen = 64; + public static final int InfoFullDirectoryLen = 68; + public static final int InfoNamesLen = 12; + public static final int InfoDirectoryBothLen = 94; + public static final int InfoMacHfsLen = 120; + + /** + * Pack a file information object into the specified buffer, using information level 1 format. + * + * @param info File information to be packed. + * @param buf Data buffer to pack the file information into + * @param infoLevel File information level. + * @param uni Pack Unicode strings if true, else pack ASCII strings + * @return Length of data packed + */ + public final static int packInfo(FileInfo info, DataBuffer buf, int infoLevel, boolean uni) + throws UnsupportedInfoLevelException + { + + // Determine the information level + + int curPos = buf.getPosition(); + + switch (infoLevel) + { + + // Standard information + + case InfoStandard: + packInfoStandard(info, buf, false, uni); + break; + + // Standard information + EA list size + + case InfoQueryEASize: + packInfoStandard(info, buf, true, uni); + break; + + // File name information + + case InfoNames: + packInfoFileName(info, buf, uni); + break; + + // File/directory information + + case InfoDirectory: + packInfoDirectory(info, buf, uni); + break; + + // Full file/directory information + + case InfoFullDirectory: + packInfoDirectoryFull(info, buf, uni); + break; + + // Full file/directory information with short name + + case InfoDirectoryBoth: + packInfoDirectoryBoth(info, buf, uni); + break; + + // Pack Macintosh format file information + + case InfoMacHfsInfo: + packInfoMacHfs(info, buf, uni); + break; + } + + // Check if we packed any data + + if (curPos == buf.getPosition()) + throw new UnsupportedInfoLevelException(); + + // Return the length of the packed data + + return buf.getPosition() - curPos; + } + + /** + * Calculate the file name offset for the specified information level. + * + * @param infoLev int + * @param offset int + * @return int + */ + public final static int calcFileNameOffset(int infoLev, int offset) + { + + // Determine the information level + + int pos = offset; + + switch (infoLev) + { + + // Standard information level + + case InfoStandard: + pos += InfoStandard; + break; + + // Standard + EA size + + case InfoQueryEASize: + pos += InfoQueryEASizeLen; + break; + + // File name information + + case InfoNames: + pos += InfoNamesLen; + break; + + // File/directory information + + case InfoDirectory: + pos += InfoDirectoryLen; + break; + + // File/directory information full + + case InfoFullDirectory: + pos += InfoFullDirectoryLen; + break; + + // Full file/directory information full plus short name + + case InfoDirectoryBoth: + pos += InfoDirectoryBothLen; + break; + } + + // Return the file name offset + + return pos; + } + + /** + * Calculate the required buffer space for the file information at the specified file + * information level. + * + * @param info File information + * @param infoLev File information level requested. + * @param resKey true if resume keys are being returned, else false. + * @param uni true if Unicode strings are being used, or false for ASCII strings + * @return int Buffer space required, or -1 if unknown information level. + */ + public final static int calcInfoSize(FileInfo info, int infoLev, boolean resKey, boolean uni) + { + + // Determine the information level requested + + int len = -1; + int nameLen = info.getFileName().length() + 1; + if (uni) + nameLen *= 2; + + switch (infoLev) + { + + // Standard information level + + case InfoStandard: + len = InfoStandardLen + nameLen; + break; + + // Standard + EA size + + case InfoQueryEASize: + len = InfoQueryEASizeLen + nameLen; + break; + + // File name information + + case InfoNames: + len += InfoNamesLen + nameLen; + break; + + // File/directory information + + case InfoDirectory: + len = InfoDirectoryLen + nameLen; + break; + + // File/directory information full + + case InfoFullDirectory: + len += InfoFullDirectoryLen + nameLen; + break; + + // Full file/directory information plus short name + + case InfoDirectoryBoth: + len = InfoDirectoryBothLen + nameLen; + break; + + // Maacintosh information level + + case InfoMacHfsInfo: + len = InfoMacHfsLen + nameLen; + break; + } + + // Add extra space for the resume key, if enabled + + if (resKey) + len += 4; + + // Return the buffer length required. + + return len; + } + + /** + * Clear the next structure offset + * + * @param dataBuf DataBuffer + * @param level int + * @param offset int + */ + public static final void clearNextOffset(DataBuffer buf, int level, int offset) + { + + // Standard information level does not have a next entry offset + + if (level == InfoStandard) + return; + + // Clear the next entry offset + + int curPos = buf.getPosition(); + buf.setPosition(offset); + buf.putInt(0); + buf.setPosition(curPos); + } + + /** + * Pack a file information object into the specified buffer. Use the standard information level + * if the EA size flag is false, else add the EA size field. + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param EAflag Add EA size field if true. + * @param uni Pack Unicode strings if true, else pack ASCII strings + */ + protected final static void packInfoStandard(FileInfo info, DataBuffer buf, boolean EAflag, boolean uni) + { + + // Information format :- + // SMB_DATE CreationDate + // SMB_TIME CreationTime + // SMB_DATE LastAccessDate + // SMB_TIME LastAccessTime + // SMB_DATE LastWriteDate + // SMB_TIME LastWriteTime + // ULONG File size + // ULONG Allocation size + // USHORT File attributes + // [ ULONG EA size ] + // UCHAR File name length + // STRING File name, null terminated + + // Pack the creation date/time + + SMBDate date = new SMBDate(0); + + if (info.hasCreationDateTime()) + { + date.setTime(info.getCreationDateTime()); + buf.putShort(date.asSMBDate()); + buf.putShort(date.asSMBTime()); + } + else + buf.putZeros(4); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + date.setTime(info.getAccessDateTime()); + buf.putShort(date.asSMBDate()); + buf.putShort(date.asSMBTime()); + } + else + buf.putZeros(4); + + // Pack the last write date/time + + if (info.hasModifyDateTime()) + { + date.setTime(info.getModifyDateTime()); + buf.putShort(date.asSMBDate()); + buf.putShort(date.asSMBTime()); + } + else + buf.putZeros(4); + + // Pack the file size and allocation size + + buf.putInt(info.getSizeInt()); + + if (info.getAllocationSize() < info.getSize()) + buf.putInt(info.getSizeInt()); + else + buf.putInt(info.getAllocationSizeInt()); + + // Pack the file attributes + + buf.putShort(info.getFileAttributes()); + + // Pack the EA size, always zero + + if (EAflag) + buf.putInt(0); + + // Pack the file name + + if (uni == true) + { + + // Pack the number of bytes followed by the Unicode name word aligned + + buf.putByte(info.getFileName().length() * 2); + buf.wordAlign(); + buf.putString(info.getFileName(), uni, true); + } + else + { + + // Pack the number of bytes followed by the ASCII name + + buf.putByte(info.getFileName().length()); + buf.putString(info.getFileName(), uni, true); + } + } + + /** + * Pack the file name information + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param uni Pack Unicode strings if true, else pack ASCII strings + */ + protected final static void packInfoFileName(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG NextEntryOffset + // ULONG FileIndex + // ULONG FileNameLength + // STRING FileName + + // Pack the file id + + int startPos = buf.getPosition(); + buf.putZeros(4); + buf.putInt(EnableFileIdPacking ? info.getFileId() : 0); + + // Pack the file name length + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + + // Pack the long file name string + + buf.putString(info.getFileName(), uni, false); + + // Align the buffer pointer and set the offset to the next file information entry + + buf.longwordAlign(); + + int curPos = buf.getPosition(); + buf.setPosition(startPos); + buf.putInt(curPos - startPos); + buf.setPosition(curPos); + } + + /** + * Pack the file/directory information + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param uni Pack Unicode strings if true, else pack ASCII strings + */ + protected final static void packInfoDirectory(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG NextEntryOffset + // ULONG FileIndex + // LARGE_INTEGER CreationTime + // LARGE_INTEGER LastAccessTime + // LARGE_INTEGER LastWriteTime + // LARGE_INTEGER ChangeTime + // LARGE_INTEGER EndOfFile + // LARGE_INTEGER AllocationSize + // ULONG FileAttributes + // ULONG FileNameLength + // STRING FileName + + // Pack the file id + + int startPos = buf.getPosition(); + buf.putZeros(4); + buf.putInt(EnableFileIdPacking ? info.getFileId() : 0); + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getAccessDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write date/time and change time + + if (info.hasModifyDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + } + else + buf.putZeros(16); + + // Pack the file size and allocation size + + buf.putLong(info.getSize()); + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Pack the file name length + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + + // Pack the long file name string + + buf.putString(info.getFileName(), uni, false); + + // Align the buffer pointer and set the offset to the next file information entry + + buf.longwordAlign(); + + int curPos = buf.getPosition(); + buf.setPosition(startPos); + buf.putInt(curPos - startPos); + buf.setPosition(curPos); + } + + /** + * Pack the full file/directory information + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param uni Pack Unicode strings if true, else pack ASCII strings + */ + protected final static void packInfoDirectoryFull(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG NextEntryOffset + // ULONG FileIndex + // LARGE_INTEGER CreationTime + // LARGE_INTEGER LastAccessTime + // LARGE_INTEGER LastWriteTime + // LARGE_INTEGER ChangeTime + // LARGE_INTEGER EndOfFile + // LARGE_INTEGER AllocationSize + // ULONG FileAttributes + // ULONG FileNameLength + // ULONG EaSize + // STRING FileName + + // Pack the file id + + int startPos = buf.getPosition(); + buf.putZeros(4); + buf.putInt(EnableFileIdPacking ? info.getFileId() : 0); + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getAccessDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write date/time + + if (info.hasModifyDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + } + else + buf.putZeros(16); + + // Pack the file size and allocation size + + buf.putLong(info.getSize()); + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Pack the file name length + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + + // Pack the EA size + + buf.putZeros(4); + + // Pack the long file name string + + buf.putString(info.getFileName(), uni, false); + + // Align the buffer pointer and set the offset to the next file information entry + + buf.longwordAlign(); + + int curPos = buf.getPosition(); + buf.setPosition(startPos); + buf.putInt(curPos - startPos); + buf.setPosition(curPos); + } + + /** + * Pack the full file/directory information + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param uni Pack Unicode strings if true, else pack ASCII strings + */ + protected final static void packInfoDirectoryBoth(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG NextEntryOffset + // ULONG FileIndex + // LARGE_INTEGER CreationTime + // LARGE_INTEGER LastAccessTime + // LARGE_INTEGER LastWriteTime + // LARGE_INTEGER ChangeTime + // LARGE_INTEGER EndOfFile + // LARGE_INTEGER AllocationSize + // ULONG FileAttributes + // ULONG FileNameLength + // ULONG EaSize + // UCHAR ShortNameLength + // WCHAR ShortName[12] + // STRING FileName + + // Pack the file id + + int startPos = buf.getPosition(); + buf.putZeros(4); + buf.putInt(EnableFileIdPacking ? info.getFileId() : 0); + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getAccessDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write date/time and change time + + if (info.hasModifyDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + } + else + buf.putZeros(16); + + // Pack the file size and allocation size + + buf.putLong(info.getSize()); + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Pack the file name length + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + + // Pack the EA size + + buf.putZeros(4); + + // Pack the short file name length (8.3 name) + + pack8Dot3Name(buf, info.getFileName(), uni); + + // Pack the long file name string + + buf.putString(info.getFileName(), uni, false); + + // Align the buffer pointer and set the offset to the next file information entry + + buf.longwordAlign(); + + int curPos = buf.getPosition(); + buf.setPosition(startPos); + buf.putInt(curPos - startPos); + buf.setPosition(curPos); + } + + /** + * Pack the Macintosh format file/directory information + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param uni Pack Unicode strings if true, else pack ASCII strings + */ + protected final static void packInfoMacHfs(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG NextEntryOffset + // ULONG FileIndex + // LARGE_INTEGER CreationTime + // LARGE_INTEGER LastWriteTime + // LARGE_INTEGER ChangeTime + // LARGE_INTEGER Data stream length + // LARGE_INTEGER Resource stream length + // LARGE_INTEGER Data stream allocation size + // LARGE_INTEGER Resource stream allocation size + // ULONG ExtFileAttributes + // UCHAR FLAttrib Macintosh SetFLock, 1 = file locked + // UCHAR Pad + // UWORD DrNmFls Number of items in a directory, zero for files + // ULONG AccessControl + // UCHAR FinderInfo[32] + // ULONG FileNameLength + // UCHAR ShortNameLength + // UCHAR Pad + // WCHAR ShortName[12] + // STRING FileName + // LONG UniqueId + + // Pack the file id + + int startPos = buf.getPosition(); + buf.putZeros(4); + buf.putInt(EnableFileIdPacking ? info.getFileId() : 0); + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write date/time and change time + + if (info.hasModifyDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + buf.putLong(NTTime.toNTTime(info.getModifyDateTime())); + } + else + buf.putZeros(16); + + // Pack the data stream size and resource stream size (always zero) + + buf.putLong(info.getSize()); + buf.putZeros(8); + + // Pack the data stream allocation size and resource stream allocation size (always zero) + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + buf.putZeros(8); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Pack the file lock and padding byte + + buf.putZeros(2); + + // Pack the number of items in a directory, always zero for now + + buf.putShort(0); + + // Pack the access control + + buf.putInt(0); + + // Pack the finder information + + buf.putZeros(32); + + // Pack the file name length + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + + // Pack the short file name length (8.3 name) and name + + pack8Dot3Name(buf, info.getFileName(), uni); + + // Pack the long file name string + + buf.putString(info.getFileName(), uni, false); + + // Pack the unique id + + buf.putInt(0); + + // Align the buffer pointer and set the offset to the next file information entry + + buf.longwordAlign(); + + int curPos = buf.getPosition(); + buf.setPosition(startPos); + buf.putInt(curPos - startPos); + buf.setPosition(curPos); + } + + /** + * Pack a file name as a short 8.3 DOS style name. Packs the short name length byte, reserved + * byte and 8.3 file name string. + * + * @param buf DataBuffer + * @param fileName String + * @param uni boolean + */ + private static final void pack8Dot3Name(DataBuffer buf, String fileName, boolean uni) + { + + if (Enable8Dot3Names == false) + { + + // Pack an emty 8.3 name structure + + buf.putZeros(26); + } + else + { + + // Split the file name string into name and extension + + int pos = fileName.lastIndexOf('.'); + + String namePart = null; + String extPart = null; + + if (pos != -1) + { + + // Split the file name string + + namePart = fileName.substring(0, pos); + extPart = fileName.substring(pos + 1); + } + else + namePart = fileName; + + // If the name already fits into an 8.3 name we do not need to pack the short name + + if (namePart.length() <= 8 && (extPart == null || extPart.length() <= 3)) + { + + // Pack an emty 8.3 name structure + + buf.putZeros(26); + return; + } + + // Truncate the name and extension parts down to 8.3 sizes + + if (namePart.length() > 8) + namePart = namePart.substring(0, 6) + "~1"; + + if (extPart != null && extPart.length() > 3) + extPart = extPart.substring(0, 3); + + // Build the 8.3 format string + + StringBuffer str = new StringBuffer(16); + + str.append(namePart); + while (str.length() < 8) + str.append(" "); + + if (extPart != null) + { + str.append("."); + str.append(extPart); + } + else + str.append(" "); + + // Space pad the string to 12 characters + + while (str.length() < 12) + str.append(" "); + + // Calculate the used length + + int len = namePart.length(); + if (extPart != null) + len = extPart.length() + 9; + + len *= 2; + + // Pack the 8.3 file name structure, always packed as Unicode + + buf.putByte(len); + buf.putByte(0); + + buf.putString(str.toString(), true, false); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/IPCHandler.java b/source/java/org/alfresco/filesys/smb/server/IPCHandler.java new file mode 100644 index 0000000000..0637202b4f --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/IPCHandler.java @@ -0,0 +1,658 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; + +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.TooManyFilesException; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.TransactionNames; +import org.alfresco.filesys.smb.dcerpc.DCEPipeType; +import org.alfresco.filesys.smb.dcerpc.server.DCEPipeFile; +import org.alfresco.filesys.smb.dcerpc.server.DCEPipeHandler; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

    + * The IPCHandler class processes requests made on the IPC$ remote admin pipe. The code is shared + * amongst different SMB protocol handlers. + */ +class IPCHandler +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + /** + * Process a request made on the IPC$ remote admin named pipe. + * + * @param sess SMBSrvSession + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + public static void processIPCRequest(SMBSrvSession sess, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the received packet from the session and verify that the connection is valid + + SMBSrvPacket smbPkt = sess.getReceivePacket(); + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = smbPkt.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ Request [" + treeId + "] - cmd = " + smbPkt.getPacketTypeString()); + + // Determine the SMB command + + switch (smbPkt.getCommand()) + { + + // Open file request + + case PacketType.OpenAndX: + case PacketType.OpenFile: + procIPCFileOpen(sess, smbPkt, outPkt); + break; + + // Read file request + + case PacketType.ReadFile: + procIPCFileRead(sess, smbPkt, outPkt); + break; + + // Read AndX file request + + case PacketType.ReadAndX: + procIPCFileReadAndX(sess, smbPkt, outPkt); + break; + + // Write file request + + case PacketType.WriteFile: + procIPCFileWrite(sess, smbPkt, outPkt); + break; + + // Write AndX file request + + case PacketType.WriteAndX: + procIPCFileWriteAndX(sess, smbPkt, outPkt); + break; + + // Close file request + + case PacketType.CloseFile: + procIPCFileClose(sess, smbPkt, outPkt); + break; + + // NT create andX request + + case PacketType.NTCreateAndX: + procNTCreateAndX(sess, smbPkt, outPkt); + break; + + // Default, respond with an unsupported function error. + + default: + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + break; + } + } + + /** + * Process an IPC$ transaction request. + * + * @param tbuf SrvTransactBuffer + * @param sess SMBSrvSession + * @param outPkt SMBSrvPacket + */ + protected static void procTransaction(SrvTransactBuffer tbuf, SMBSrvSession sess, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ Transaction pipe=" + tbuf.getName() + ", subCmd=" + + NamedPipeTransaction.getSubCommand(tbuf.getFunction())); + + // Call the required transaction handler + + if (tbuf.getName().compareTo(TransactionNames.PipeLanman) == 0) + { + + // Call the \PIPE\LANMAN transaction handler to process the request + + if (PipeLanmanHandler.processRequest(tbuf, sess, outPkt)) + return; + } + + // Process the pipe command + + switch (tbuf.getFunction()) + { + + // Set named pipe handle state + + case NamedPipeTransaction.SetNmPHandState: + procSetNamedPipeHandleState(sess, tbuf, outPkt); + break; + + // Named pipe transation request, pass the request to the DCE/RPC handler + + case NamedPipeTransaction.TransactNmPipe: + DCERPCHandler.processDCERPCRequest(sess, tbuf, outPkt); + break; + + // Unknown command + + default: + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + break; + } + } + + /** + * Process a special IPC$ file open request. + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procIPCFileOpen(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the data bytes position and length + + int dataPos = rxPkt.getByteOffset(); + int dataLen = rxPkt.getByteCount(); + byte[] buf = rxPkt.getBuffer(); + + // Extract the filename string + + String fileName = DataPacker.getString(buf, dataPos, dataLen); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ Open file = " + fileName); + + // Check if the requested IPC$ file is valid + + int pipeType = DCEPipeType.getNameAsType(fileName); + if (pipeType == -1) + { + sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Get the tree connection details + + int treeId = rxPkt.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Create a network file for the special pipe + + DCEPipeFile pipeFile = new DCEPipeFile(pipeType); + pipeFile.setGrantedAccess(NetworkFile.READWRITE); + + // Add the file to the list of open files for this tree connection + + int fid = -1; + + try + { + fid = conn.addFile(pipeFile, sess); + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + sess.sendErrorResponseSMB(SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + + // Build the open file response + + outPkt.setParameterCount(15); + + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); // AndX offset + + outPkt.setParameter(2, fid); + outPkt.setParameter(3, 0); // file attributes + outPkt.setParameter(4, 0); // last write time + outPkt.setParameter(5, 0); // last write date + outPkt.setParameterLong(6, 0); // file size + outPkt.setParameter(8, 0); + outPkt.setParameter(9, 0); + outPkt.setParameter(10, 0); // named pipe state + outPkt.setParameter(11, 0); + outPkt.setParameter(12, 0); // server FID (long) + outPkt.setParameter(13, 0); + outPkt.setParameter(14, 0); + + outPkt.setByteCount(0); + + // Send the response packet + + sess.sendResponseSMB(outPkt); + } + + /** + * Process an IPC pipe file read request + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procIPCFileRead(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Check if the received packet is a valid read file request + + if (rxPkt.checkPacketIsValid(5, 0) == false) + { + + // Invalid request + + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ File Read"); + + // Pass the read request the DCE/RPC handler + + DCERPCHandler.processDCERPCRead(sess, rxPkt, outPkt); + } + + /** + * Process an IPC pipe file read andX request + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procIPCFileReadAndX(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Check if the received packet is a valid read andX file request + + if (rxPkt.checkPacketIsValid(10, 0) == false) + { + + // Invalid request + + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ File Read AndX"); + + // Pass the read request the DCE/RPC handler + + DCERPCHandler.processDCERPCRead(sess, rxPkt, outPkt); + } + + /** + * Process an IPC pipe file write request + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procIPCFileWrite(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Check if the received packet is a valid write file request + + if (rxPkt.checkPacketIsValid(5, 0) == false) + { + + // Invalid request + + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ File Write"); + + // Pass the write request the DCE/RPC handler + + DCERPCHandler.processDCERPCRequest(sess, rxPkt, outPkt); + } + + /** + * Process an IPC pipe file write andX request + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procIPCFileWriteAndX(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Check if the received packet is a valid write andX request + + if (rxPkt.checkPacketIsValid(12, 0) == false) + { + + // Invalid request + + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ File Write AndX"); + + // Pass the write request the DCE/RPC handler + + DCERPCHandler.processDCERPCRequest(sess, rxPkt, outPkt); + } + + /** + * Process a special IPC$ file close request. + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procIPCFileClose(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file close request + + if (rxPkt.checkPacketIsValid(3, 0) == false) + { + sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = rxPkt.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Get the file id from the request + + int fid = rxPkt.getParameter(0); + DCEPipeFile netFile = (DCEPipeFile) conn.findFile(fid); + + if (netFile == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("IPC$ File close [" + treeId + "] fid=" + fid); + + // Remove the file from the connections list of open files + + conn.removeFile(fid, sess); + + // Build the close file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + sess.sendResponseSMB(outPkt); + } + + /** + * Process a set named pipe handle state request + * + * @param sess SMBSrvSession + * @param tbuf SrvTransactBuffer + * @param outPkt SMBSrvPacket + */ + protected static void procSetNamedPipeHandleState(SMBSrvSession sess, SrvTransactBuffer tbuf, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the request parameters + + DataBuffer setupBuf = tbuf.getSetupBuffer(); + setupBuf.skipBytes(2); + int fid = setupBuf.getShort(); + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + int state = paramBuf.getShort(); + + // Get the connection for the request + + TreeConnection conn = sess.findConnection(tbuf.getTreeId()); + + // Get the IPC pipe file for the specified file id + + DCEPipeFile netFile = (DCEPipeFile) conn.findFile(fid); + if (netFile == null) + { + sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug(" SetNmPHandState pipe=" + netFile.getName() + ", fid=" + fid + ", state=0x" + + Integer.toHexString(state)); + + // Store the named pipe state + + netFile.setPipeState(state); + + // Setup the response packet + + SMBSrvTransPacket.initTransactReply(outPkt, 0, 0, 0, 0); + + // Send the response packet + + sess.sendResponseSMB(outPkt); + } + + /** + * Process an NT create andX request + * + * @param sess SMBSrvSession + * @param rxPkt SMBSrvPacket + * @param outPkt SMBSrvPacket + */ + protected static void procNTCreateAndX(SMBSrvSession sess, SMBSrvPacket rxPkt, SMBSrvPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = rxPkt.getTreeId(); + TreeConnection conn = sess.findConnection(treeId); + + if (conn == null) + { + sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Extract the NT create andX parameters + + NTParameterPacker prms = new NTParameterPacker(rxPkt.getBuffer(), SMBSrvPacket.PARAMWORDS + 5); + + int nameLen = prms.unpackWord(); + int flags = prms.unpackInt(); + int rootFID = prms.unpackInt(); + int accessMask = prms.unpackInt(); + long allocSize = prms.unpackLong(); + int attrib = prms.unpackInt(); + int shrAccess = prms.unpackInt(); + int createDisp = prms.unpackInt(); + int createOptn = prms.unpackInt(); + int impersonLev = prms.unpackInt(); + int secFlags = prms.unpackByte(); + + // Extract the filename string + + int pos = DataPacker.wordAlign(rxPkt.getByteOffset()); + String fileName = DataPacker.getUnicodeString(rxPkt.getBuffer(), pos, nameLen); + if (fileName == null) + { + sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NT Create AndX [" + treeId + "] name=" + fileName + ", flags=0x" + + Integer.toHexString(flags) + ", attr=0x" + Integer.toHexString(attrib) + ", allocSize=" + + allocSize); + + // Check if the pipe name is a short or long name + + if (fileName.startsWith("\\PIPE") == false) + fileName = "\\PIPE" + fileName; + + // Check if the requested IPC$ file is valid + + int pipeType = DCEPipeType.getNameAsType(fileName); + if (pipeType == -1) + { + sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.NTErr); + return; + } + + // Check if there is a handler for the pipe file + + if (DCEPipeHandler.getHandlerForType(pipeType) == null) + { + sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.NTErr); + return; + } + + // Create a network file for the special pipe + + DCEPipeFile pipeFile = new DCEPipeFile(pipeType); + pipeFile.setGrantedAccess(NetworkFile.READWRITE); + + // Add the file to the list of open files for this tree connection + + int fid = -1; + + try + { + fid = conn.addFile(pipeFile, sess); + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + sess.sendErrorResponseSMB(SMBStatus.Win32InvalidHandle, SMBStatus.NTErr); + return; + } + + // Build the NT create andX response + + outPkt.setParameterCount(34); + + prms.reset(outPkt.getBuffer(), SMBSrvPacket.PARAMWORDS + 4); + + prms.packByte(0); + prms.packWord(fid); + prms.packInt(0x0001); // File existed and was opened + + prms.packLong(0); // Creation time + prms.packLong(0); // Last access time + prms.packLong(0); // Last write time + prms.packLong(0); // Change time + + prms.packInt(0x0080); // File attributes + prms.packLong(4096); // Allocation size + prms.packLong(0); // End of file + prms.packWord(2); // File type - named pipe, message mode + prms.packByte(0xFF); // Pipe instancing count + prms.packByte(0x05); // IPC state bits + + prms.packByte(0); // directory flag + + outPkt.setByteCount(0); + + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, outPkt.getLength()); // AndX offset + + // Send the response packet + + sess.sendResponseSMB(outPkt); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/LanManProtocolHandler.java b/source/java/org/alfresco/filesys/smb/server/LanManProtocolHandler.java new file mode 100644 index 0000000000..29b21448f6 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/LanManProtocolHandler.java @@ -0,0 +1,2931 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.auth.InvalidUserException; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.core.InvalidDeviceInterfaceException; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.filesys.AccessDeniedException; +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.FileAccess; +import org.alfresco.filesys.server.filesys.FileAction; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileOfflineException; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.FileSharingException; +import org.alfresco.filesys.server.filesys.FileStatus; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.filesys.server.filesys.SrvDiskInfo; +import org.alfresco.filesys.server.filesys.TooManyConnectionsException; +import org.alfresco.filesys.server.filesys.TooManyFilesException; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.server.filesys.UnsupportedInfoLevelException; +import org.alfresco.filesys.server.filesys.VolumeInfo; +import org.alfresco.filesys.smb.DataType; +import org.alfresco.filesys.smb.FindFirstNext; +import org.alfresco.filesys.smb.InvalidUNCPathException; +import org.alfresco.filesys.smb.PCShare; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBDate; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * LanMan SMB Protocol Handler Class. + *

    + * The LanMan protocol handler processes the additional SMBs that were added to the protocol in the + * LanMan1 and LanMan2 SMB dialects. + */ +class LanManProtocolHandler extends CoreProtocolHandler +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Locking type flags + + protected static final int LockShared = 0x01; + protected static final int LockOplockRelease = 0x02; + protected static final int LockChangeType = 0x04; + protected static final int LockCancel = 0x08; + protected static final int LockLargeFiles = 0x10; + + /** + * LanManProtocolHandler constructor. + */ + protected LanManProtocolHandler() + { + super(); + } + + /** + * LanManProtocolHandler constructor. + * + * @param sess org.alfresco.filesys.smbsrv.SMBSrvSession + */ + protected LanManProtocolHandler(SMBSrvSession sess) + { + super(sess); + } + + /** + * Return the protocol name + * + * @return String + */ + public String getName() + { + return "LanMan"; + } + + /** + * Process the chained SMB commands (AndX). + * + * @return New offset to the end of the reply packet + * @param outPkt Reply packet. + */ + protected final int procAndXCommands(SMBSrvPacket outPkt) + { + + // Get the chained command and command block offset + + int andxCmd = m_smbPkt.getAndXCommand(); + int andxOff = m_smbPkt.getParameter(1) + RFCNetBIOSProtocol.HEADER_LEN; + + // Set the initial chained command and offset + + outPkt.setAndXCommand(andxCmd); + outPkt.setParameter(1, andxOff - RFCNetBIOSProtocol.HEADER_LEN); + + // Pointer to the last parameter block, starts with the main command parameter block + + int paramBlk = SMBSrvPacket.WORDCNT; + + // Get the current end of the reply packet offset + + int endOfPkt = outPkt.getByteOffset() + outPkt.getByteCount(); + boolean andxErr = false; + + while (andxCmd != SMBSrvPacket.NO_ANDX_CMD && andxErr == false) + { + + // Determine the chained command type + + int prevEndOfPkt = endOfPkt; + + switch (andxCmd) + { + + // Tree connect + + case PacketType.TreeConnectAndX: + endOfPkt = procChainedTreeConnectAndX(andxOff, outPkt, endOfPkt); + break; + } + + // Advance to the next chained command block + + andxCmd = m_smbPkt.getAndXParameter(andxOff, 0) & 0x00FF; + andxOff = m_smbPkt.getAndXParameter(andxOff, 1); + + // Set the next chained command details in the current parameter block + + outPkt.setAndXCommand(prevEndOfPkt, andxCmd); + outPkt.setAndXParameter(paramBlk, 1, prevEndOfPkt - RFCNetBIOSProtocol.HEADER_LEN); + + // Advance the current parameter block + + paramBlk = prevEndOfPkt; + + // Check if the chained command has generated an error status + + if (outPkt.getErrorCode() != SMBStatus.Success) + andxErr = true; + } + + // Return the offset to the end of the reply packet + + return endOfPkt; + } + + /** + * Process a chained tree connect request. + * + * @return New end of reply offset. + * @param cmdOff int Offset to the chained command within the request packet. + * @param outPkt SMBSrvPacket Reply packet. + * @param endOff int Offset to the current end of the reply packet. + */ + protected final int procChainedTreeConnectAndX(int cmdOff, SMBSrvPacket outPkt, int endOff) + { + + // Extract the parameters + + int flags = m_smbPkt.getAndXParameter(cmdOff, 2); + int pwdLen = m_smbPkt.getAndXParameter(cmdOff, 3); + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getAndXByteOffset(cmdOff); + int dataLen = m_smbPkt.getAndXByteCount(cmdOff); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the password string + + String pwd = null; + + if (pwdLen > 0) + { + pwd = new String(buf, dataPos, pwdLen); + dataPos += pwdLen; + dataLen -= pwdLen; + } + + // Extract the requested share name, as a UNC path + + String uncPath = DataPacker.getString(buf, dataPos, dataLen); + if (uncPath == null) + { + outPkt.setError(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return endOff; + } + + // Extract the service type string + + dataPos += uncPath.length() + 1; // null terminated + dataLen -= uncPath.length() + 1; // null terminated + + String service = DataPacker.getString(buf, dataPos, dataLen); + if (service == null) + { + outPkt.setError(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return endOff; + } + + // Convert the service type to a shared device type, client may specify '?????' in which + // case we ignore the error. + + int servType = ShareType.ServiceAsType(service); + if (servType == ShareType.UNKNOWN && service.compareTo("?????") != 0) + { + outPkt.setError(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return endOff; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("ANDX Tree Connect AndX - " + uncPath + ", " + service); + + // Parse the requested share name + + PCShare share = null; + + try + { + share = new PCShare(uncPath); + } + catch (org.alfresco.filesys.smb.InvalidUNCPathException ex) + { + outPkt.setError(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return endOff; + } + + // Map the IPC$ share to the admin pipe type + + if (servType == ShareType.NAMEDPIPE && share.getShareName().compareTo("IPC$") == 0) + servType = ShareType.ADMINPIPE; + + // Find the requested shared device + + SharedDevice shareDev = null; + + try + { + + // Get/create the shared device + + shareDev = m_sess.getSMBServer().findShare(share.getNodeName(), share.getShareName(), servType, + getSession(), true); + } + catch (InvalidUserException ex) + { + + // Return a logon failure status + + outPkt.setError(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return endOff; + } + catch (Exception ex) + { + + // Return a general status, bad network name + + outPkt.setError(SMBStatus.SRVInvalidNetworkName, SMBStatus.ErrSrv); + return endOff; + } + + // Check if the share is valid + + if (shareDev == null || (servType != ShareType.UNKNOWN && shareDev.getType() != servType)) + { + outPkt.setError(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return endOff; + } + + // Authenticate the share connect, if the server is using share mode security + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + int filePerm = FileAccess.Writeable; + + if (auth != null && auth.getAccessMode() == SrvAuthenticator.SHARE_MODE) + { + + // Validate the share connection + + filePerm = auth.authenticateShareConnect(m_sess.getClientInformation(), shareDev, pwd, m_sess); + if (filePerm < 0) + { + + // Invalid share connection request + + outPkt.setError(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return endOff; + } + } + + // Allocate a tree id for the new connection + + try + { + + // Allocate the tree id for this connection + + int treeId = m_sess.addConnection(shareDev); + outPkt.setTreeId(treeId); + + // Set the file permission that this user has been granted for this share + + TreeConnection tree = m_sess.findConnection(treeId); + tree.setPermission(filePerm); + + // Inform the driver that a connection has been opened + + if (tree.getInterface() != null) + tree.getInterface().treeOpened(m_sess, tree); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("ANDX Tree Connect AndX - Allocated Tree Id = " + treeId); + } + catch (TooManyConnectionsException ex) + { + + // Too many connections open at the moment + + outPkt.setError(SMBStatus.SRVNoResourcesAvailable, SMBStatus.ErrSrv); + return endOff; + } + + // Build the tree connect response + + outPkt.setAndXParameterCount(endOff, 2); + outPkt.setAndXParameter(endOff, 0, SMBSrvPacket.NO_ANDX_CMD); + outPkt.setAndXParameter(endOff, 1, 0); + + // Pack the service type + + int pos = outPkt.getAndXByteOffset(endOff); + byte[] outBuf = outPkt.getBuffer(); + pos = DataPacker.putString(ShareType.TypeAsService(shareDev.getType()), outBuf, pos, true); + int bytLen = pos - outPkt.getAndXByteOffset(endOff); + outPkt.setAndXByteCount(endOff, bytLen); + + // Return the new end of packet offset + + return pos; + } + + /** + * Close a search started via the transact2 find first/next command. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procFindClose(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid find close request + + if (m_smbPkt.checkPacketIsValid(1, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the search id + + int searchId = m_smbPkt.getParameter(0); + + // Get the search context + + SearchContext ctx = m_sess.getSearchContext(searchId); + + if (ctx == null) + { + + // Invalid search handle + + m_sess.sendSuccessResponseSMB(); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Close trans search [" + searchId + "]"); + + // Deallocate the search slot, close the search. + + m_sess.deallocateSearchSlot(searchId); + + // Return a success status SMB + + m_sess.sendSuccessResponseSMB(); + } + + /** + * Process the file lock/unlock request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procLockingAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid locking andX request + + if (m_smbPkt.checkPacketIsValid(8, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Extract the file lock/unlock parameters + + int fid = m_smbPkt.getParameter(2); + int lockType = m_smbPkt.getParameter(3); + long lockTmo = m_smbPkt.getParameterLong(4); + int lockCnt = m_smbPkt.getParameter(6); + int unlockCnt = m_smbPkt.getParameter(7); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_LOCK)) + logger.debug("File Lock [" + netFile.getFileId() + "] : type=0x" + Integer.toHexString(lockType) + ", tmo=" + + lockTmo + ", locks=" + lockCnt + ", unlocks=" + unlockCnt); + + // Return a success status for now + + outPkt.setParameterCount(2); + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); + outPkt.setByteCount(0); + + // Send the lock request response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the logoff request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procLogoffAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid logoff andX request + + if (m_smbPkt.checkPacketIsValid(15, 1) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Return a success status SMB + + m_sess.sendSuccessResponseSMB(); + } + + /** + * Process the file open request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procOpenAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid open andX request + + if (m_smbPkt.checkPacketIsValid(15, 1) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. If the device is + // not a disk type device then return an error. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + else if (conn.getSharedDevice().getType() != ShareType.DISK) + { + + // Return an access denied error + + // m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + // m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Extract the open file parameters + + int flags = m_smbPkt.getParameter(2); + int access = m_smbPkt.getParameter(3); + int srchAttr = m_smbPkt.getParameter(4); + int fileAttr = m_smbPkt.getParameter(5); + int crTime = m_smbPkt.getParameter(6); + int crDate = m_smbPkt.getParameter(7); + int openFunc = m_smbPkt.getParameter(8); + int allocSiz = m_smbPkt.getParameterLong(9); + + // Extract the filename string + + String fileName = m_smbPkt.unpackString(m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Create the file open parameters + + SMBDate crDateTime = null; + if (crTime > 0 && crDate > 0) + crDateTime = new SMBDate(crDate, crTime); + + FileOpenParams params = new FileOpenParams(fileName, openFunc, access, srchAttr, fileAttr, allocSiz, crDateTime + .getTime()); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Open AndX [" + treeId + "] params=" + params); + + // Access the disk interface and open the requested file + + int fid; + NetworkFile netFile = null; + int respAction = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Check if the requested file already exists + + int fileSts = disk.fileExists(m_sess, conn, fileName); + + if (fileSts == FileStatus.NotExist) + { + + // Check if the file should be created if it does not exist + + if (FileAction.createNotExists(openFunc)) + { + + // Check if the session has write access to the filesystem + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Create a new file + + netFile = disk.createFile(m_sess, conn, params); + + // Indicate that the file did not exist and was created + + respAction = FileAction.FileCreated; + } + else + { + + // Check if the path is a directory + + if (fileSts == FileStatus.DirectoryExists) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + } + else + { + + // Return a file not found error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + } + return; + } + } + else + { + + // Open the requested file + + netFile = disk.openFile(m_sess, conn, params); + + // Set the file action response + + if (FileAction.truncateExistingFile(openFunc)) + respAction = FileAction.FileTruncated; + else + respAction = FileAction.FileExisted; + } + + // Add the file to the list of open files for this tree connection + + fid = conn.addFile(netFile, getSession()); + + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + m_sess.sendErrorResponseSMB(SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (FileSharingException ex) + { + + // Return a sharing violation error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileSharingConflict, SMBStatus.ErrDos); + return; + } + catch (FileOfflineException ex) + { + + // File data is unavailable + + m_sess.sendErrorResponseSMB(SMBStatus.NTFileOffline, SMBStatus.HRDDriveNotReady, SMBStatus.ErrHrd); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the open file response + + outPkt.setParameterCount(15); + + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); // AndX offset + + outPkt.setParameter(2, fid); + outPkt.setParameter(3, netFile.getFileAttributes()); // file attributes + + SMBDate modDate = null; + + if (netFile.hasModifyDate()) + modDate = new SMBDate(netFile.getModifyDate()); + + outPkt.setParameter(4, modDate != null ? modDate.asSMBTime() : 0); // last write time + outPkt.setParameter(5, modDate != null ? modDate.asSMBDate() : 0); // last write date + outPkt.setParameterLong(6, netFile.getFileSizeInt()); // file size + outPkt.setParameter(8, netFile.getGrantedAccess()); + outPkt.setParameter(9, OpenAndX.FileTypeDisk); + outPkt.setParameter(10, 0); // named pipe state + outPkt.setParameter(11, respAction); + outPkt.setParameter(12, 0); // server FID (long) + outPkt.setParameter(13, 0); + outPkt.setParameter(14, 0); + + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the file read request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procReadAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid read andX request + + if (m_smbPkt.checkPacketIsValid(10, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + + // Extract the read file parameters + + int fid = m_smbPkt.getParameter(2); + int offset = m_smbPkt.getParameterLong(3); + int maxCount = m_smbPkt.getParameter(5); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Read AndX [" + netFile.getFileId() + "] : Size=" + maxCount + " ,Pos=" + offset); + + // Read data from the file + + byte[] buf = outPkt.getBuffer(); + int dataPos = 0; + int rdlen = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Set the returned parameter count so that the byte offset can be calculated + + outPkt.setParameterCount(12); + dataPos = outPkt.getByteOffset(); + // dataPos = ( dataPos + 3) & 0xFFFFFFFC; // longword align the data + + // Check if the requested data length will fit into the buffer + + int dataLen = buf.length - dataPos; + if (dataLen < maxCount) + maxCount = dataLen; + + // Read from the file + + rdlen = disk.readFile(m_sess, conn, netFile, buf, dataPos, maxCount, offset); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // No access to file, or file is a directory + // + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Read Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + logger.error("File Read Error [" + netFile.getFileId() + "] : ", ex); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDReadFault, SMBStatus.ErrHrd); + return; + } + + // Return the data block + + outPkt.setAndXCommand(0xFF); // no chained command + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 0); // bytes remaining, for pipes only + outPkt.setParameter(3, 0); // data compaction mode + outPkt.setParameter(4, 0); // reserved + outPkt.setParameter(5, rdlen); // data length + outPkt.setParameter(6, dataPos - RFCNetBIOSProtocol.HEADER_LEN); + // offset to data + + // Clear the reserved parameters + + for (int i = 7; i < 12; i++) + outPkt.setParameter(i, 0); + + // Set the byte count + + outPkt.setByteCount((dataPos + rdlen) - outPkt.getByteOffset()); + + // Send the read andX response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Rename a file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procRenameFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid rename file request + + if (m_smbPkt.checkPacketIsValid(1, 4) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the Unicode flag + + boolean isUni = m_smbPkt.isUnicode(); + + // Read the data block + + m_smbPkt.resetBytePointer(); + + // Extract the old file name + + if (m_smbPkt.unpackByte() != DataType.ASCII) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + String oldName = m_smbPkt.unpackString(isUni); + if (oldName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Extract the new file name + + if (m_smbPkt.unpackByte() != DataType.ASCII) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + String newName = m_smbPkt.unpackString(isUni); + if (oldName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Rename [" + treeId + "] old name=" + oldName + ", new name=" + newName); + + // Access the disk interface and rename the requested file + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Rename the requested file + + disk.renameFile(m_sess, conn, oldName, newName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the rename file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the SMB session setup request. + * + * @param outPkt Response SMB packet. + */ + protected void procSessionSetup(SMBSrvPacket outPkt) throws SMBSrvException, IOException, + TooManyConnectionsException + { + + // Extract the client details from the session setup request + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the session details + + int maxBufSize = m_smbPkt.getParameter(2); + int maxMpx = m_smbPkt.getParameter(3); + int vcNum = m_smbPkt.getParameter(4); + + // Extract the password string + + byte[] pwd = null; + int pwdLen = m_smbPkt.getParameter(7); + + if (pwdLen > 0) + { + pwd = new byte[pwdLen]; + for (int i = 0; i < pwdLen; i++) + pwd[i] = buf[dataPos + i]; + dataPos += pwdLen; + dataLen -= pwdLen; + } + + // Extract the user name string + + String user = DataPacker.getString(buf, dataPos, dataLen); + if (user == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + else + { + + // Update the buffer pointers + + dataLen -= user.length() + 1; + dataPos += user.length() + 1; + } + + // Extract the clients primary domain name string + + String domain = ""; + + if (dataLen > 0) + { + + // Extract the callers domain name + + domain = DataPacker.getString(buf, dataPos, dataLen); + if (domain == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + else + { + + // Update the buffer pointers + + dataLen -= domain.length() + 1; + dataPos += domain.length() + 1; + } + } + + // Extract the clients native operating system + + String clientOS = ""; + + if (dataLen > 0) + { + + // Extract the callers operating system name + + clientOS = DataPacker.getString(buf, dataPos, dataLen); + if (clientOS == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + } + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + logger.debug("Session setup from user=" + user + ", password=" + pwd + ", domain=" + domain + ", os=" + + clientOS + ", VC=" + vcNum + ", maxBuf=" + maxBufSize + ", maxMpx=" + maxMpx); + + // Store the client maximum buffer size and maximum multiplexed requests count + + m_sess.setClientMaximumBufferSize(maxBufSize); + m_sess.setClientMaximumMultiplex(maxMpx); + + // Create the client information and store in the session + + ClientInfo client = new ClientInfo(user, pwd); + client.setDomain(domain); + client.setOperatingSystem(clientOS); + if (m_sess.hasRemoteAddress()) + client.setClientAddress(m_sess.getRemoteAddress().getHostAddress()); + + if (m_sess.getClientInformation() == null) + { + + // Set the session client details + + m_sess.setClientInformation(client); + } + else + { + + // Get the current client details from the session + + ClientInfo curClient = m_sess.getClientInformation(); + + if (curClient.getUserName() == null || curClient.getUserName().length() == 0) + { + + // Update the client information + + m_sess.setClientInformation(client); + } + else + { + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + logger.debug("Session already has client information set"); + } + } + + // Authenticate the user, if the server is using user mode security + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + boolean isGuest = false; + + if (auth != null && auth.getAccessMode() == SrvAuthenticator.USER_MODE) + { + + // Validate the user + + int sts = auth.authenticateUser(client, m_sess, SrvAuthenticator.LANMAN); + if (sts > 0 && (sts & SrvAuthenticator.AUTH_GUEST) != 0) + isGuest = true; + else if (sts != SrvAuthenticator.AUTH_ALLOW) + { + + // Invalid user, reject the session setup request + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + } + + // Set the guest flag for the client and logged on status + + client.setGuest(isGuest); + getSession().setLoggedOn(true); + + // Build the session setup response SMB + + outPkt.setParameterCount(3); + outPkt.setParameter(0, 0); // No chained response + outPkt.setParameter(1, 0); // Offset to chained response + outPkt.setParameter(2, isGuest ? 1 : 0); + outPkt.setByteCount(0); + + outPkt.setTreeId(0); + outPkt.setUserId(0); + + // Set the various flags + + // outPkt.setFlags( SMBSrvPacket.FLG_CASELESS); + int flags = outPkt.getFlags(); + flags &= ~SMBSrvPacket.FLG_CASELESS; + outPkt.setFlags(flags); + outPkt.setFlags2(SMBSrvPacket.FLG2_LONGFILENAMES); + + // Pack the OS, dialect and domain name strings. + + int pos = outPkt.getByteOffset(); + buf = outPkt.getBuffer(); + + pos = DataPacker.putString("Java", buf, pos, true); + pos = DataPacker.putString("JLAN Server " + m_sess.getServer().isVersion(), buf, pos, true); + pos = DataPacker.putString(m_sess.getServer().getConfiguration().getDomainName(), buf, pos, true); + + outPkt.setByteCount(pos - outPkt.getByteOffset()); + + // Check if there is a chained command, or commands + + if (m_smbPkt.hasAndXCommand() && dataPos < m_smbPkt.getReceivedLength()) + { + + // Process any chained commands, AndX + + pos = procAndXCommands(outPkt); + } + else + { + + // Indicate that there are no chained replies + + outPkt.setAndXCommand(SMBSrvPacket.NO_ANDX_CMD); + } + + // Send the negotiate response + + m_sess.sendResponseSMB(outPkt, pos); + + // Update the session state + + m_sess.setState(SMBSrvSessionState.SMBSESSION); + + // Notify listeners that a user has logged onto the session + + m_sess.getSMBServer().sessionLoggedOn(m_sess); + } + + /** + * Process a transact2 request. The transact2 can contain many different sub-requests. + * + * @param outPkt SMBSrvPacket + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procTransact2(SMBSrvPacket outPkt) throws IOException, SMBSrvException + { + + // Check that we received enough parameters for a transact2 request + + if (m_smbPkt.checkPacketIsValid(15, 0) == false) + { + + // Not enough parameters for a valid transact2 request + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Create a transact packet using the received SMB packet + + SMBSrvTransPacket tranPkt = new SMBSrvTransPacket(m_smbPkt.getBuffer()); + + // Create a transact buffer to hold the transaction setup, parameter and data blocks + + SrvTransactBuffer transBuf = null; + int subCmd = tranPkt.getSubFunction(); + + if (tranPkt.getTotalParameterCount() == tranPkt.getParameterBlockCount() + && tranPkt.getTotalDataCount() == tranPkt.getDataBlockCount()) + { + + // Create a transact buffer using the packet buffer, the entire request is contained in + // a single + // packet + + transBuf = new SrvTransactBuffer(tranPkt); + } + else + { + + // Create a transact buffer to hold the multiple transact request parameter/data blocks + + transBuf = new SrvTransactBuffer(tranPkt.getSetupCount(), tranPkt.getTotalParameterCount(), tranPkt + .getTotalDataCount()); + transBuf.setType(tranPkt.getCommand()); + transBuf.setFunction(subCmd); + + // Append the setup, parameter and data blocks to the transaction data + + byte[] buf = tranPkt.getBuffer(); + + transBuf.appendSetup(buf, tranPkt.getSetupOffset(), tranPkt.getSetupCount() * 2); + transBuf.appendParameter(buf, tranPkt.getParameterBlockOffset(), tranPkt.getParameterBlockCount()); + transBuf.appendData(buf, tranPkt.getDataBlockOffset(), tranPkt.getDataBlockCount()); + } + + // Set the return data limits for the transaction + + transBuf.setReturnLimits(tranPkt.getMaximumReturnSetupCount(), tranPkt.getMaximumReturnParameterCount(), + tranPkt.getMaximumReturnDataCount()); + + // Check for a multi-packet transaction, for a multi-packet transaction we just acknowledge + // the receive with + // an empty response SMB + + if (transBuf.isMultiPacket()) + { + + // Save the partial transaction data + + m_sess.setTransaction(transBuf); + + // Send an intermediate acknowedgement response + + m_sess.sendSuccessResponseSMB(); + return; + } + + // Check if the transaction is on the IPC$ named pipe, the request requires special + // processing + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + IPCHandler.procTransaction(transBuf, m_sess, outPkt); + return; + } + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction [" + treeId + "] tbuf=" + transBuf); + + // Process the transaction buffer + + processTransactionBuffer(transBuf, outPkt); + } + + /** + * Process a transact2 secondary request. + * + * @param outPkt SMBSrvPacket + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procTransact2Secondary(SMBSrvPacket outPkt) throws IOException, SMBSrvException + { + + // Check that we received enough parameters for a transact2 request + + if (m_smbPkt.checkPacketIsValid(8, 0) == false) + { + + // Not enough parameters for a valid transact2 request + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Check if there is an active transaction, and it is an NT transaction + + if (m_sess.hasTransaction() == false + || (m_sess.getTransaction().isType() == PacketType.Transaction && m_smbPkt.getCommand() != PacketType.TransactionSecond) + || (m_sess.getTransaction().isType() == PacketType.Transaction2 && m_smbPkt.getCommand() != PacketType.Transaction2Second)) + { + + // No transaction to continue, or packet does not match the existing transaction, return + // an error + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Create an NT transaction using the received packet + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(m_smbPkt.getBuffer()); + byte[] buf = tpkt.getBuffer(); + SrvTransactBuffer transBuf = m_sess.getTransaction(); + + // Append the parameter data to the transaction buffer, if any + + int plen = tpkt.getSecondaryParameterBlockCount(); + if (plen > 0) + { + + // Append the data to the parameter buffer + + DataBuffer paramBuf = transBuf.getParameterBuffer(); + paramBuf.appendData(buf, tpkt.getSecondaryParameterBlockOffset(), plen); + } + + // Append the data block to the transaction buffer, if any + + int dlen = tpkt.getSecondaryDataBlockCount(); + if (dlen > 0) + { + + // Append the data to the data buffer + + DataBuffer dataBuf = transBuf.getDataBuffer(); + dataBuf.appendData(buf, tpkt.getSecondaryDataBlockOffset(), dlen); + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction Secondary [" + treeId + "] paramLen=" + plen + ", dataLen=" + dlen); + + // Check if the transaction has been received or there are more sections to be received + + int totParam = tpkt.getTotalParameterCount(); + int totData = tpkt.getTotalDataCount(); + + int paramDisp = tpkt.getParameterBlockDisplacement(); + int dataDisp = tpkt.getDataBlockDisplacement(); + + if ((paramDisp + plen) == totParam && (dataDisp + dlen) == totData) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction complete, processing ..."); + + // Clear the in progress transaction + + m_sess.setTransaction(null); + + // Check if the transaction is on the IPC$ named pipe, the request requires special + // processing + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + IPCHandler.procTransaction(transBuf, m_sess, outPkt); + return; + } + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction second [" + treeId + "] tbuf=" + transBuf); + + // Process the transaction + + processTransactionBuffer(transBuf, outPkt); + } + else + { + + // There are more transaction parameter/data sections to be received, return an + // intermediate response + + m_sess.sendSuccessResponseSMB(); + } + } + + /** + * Process a transaction buffer + * + * @param tbuf TransactBuffer + * @param outPkt SMBSrvPacket + * @exception IOException If a network error occurs + * @exception SMBSrvException If an SMB error occurs + */ + private final void processTransactionBuffer(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws IOException, + SMBSrvException + { + + // Get the transaction sub-command code and validate + + switch (tbuf.getFunction()) + { + + // Start a file search + + case PacketType.Trans2FindFirst: + procTrans2FindFirst(tbuf, outPkt); + break; + + // Continue a file search + + case PacketType.Trans2FindNext: + procTrans2FindNext(tbuf, outPkt); + break; + + // Query file system information + + case PacketType.Trans2QueryFileSys: + procTrans2QueryFileSys(tbuf, outPkt); + break; + + // Query path + + case PacketType.Trans2QueryPath: + procTrans2QueryPath(tbuf, outPkt); + break; + + // Unknown transact2 command + + default: + + // Return an unrecognized command error + + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + break; + } + } + + /** + * Process a transact2 file search request. + * + * @param tbuf Transaction request details + * @param outPkt Packet to use for the reply. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2FindFirst(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the search parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int srchAttr = paramBuf.getShort(); + int maxFiles = paramBuf.getShort(); + int srchFlag = paramBuf.getShort(); + int infoLevl = paramBuf.getShort(); + paramBuf.skipBytes(4); + + String srchPath = paramBuf.getString(tbuf.isUnicode()); + + // Check if the search path is valid + + if (srchPath == null || srchPath.length() == 0) + { + + // Invalid search request + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Access the shared device disk interface + + SearchContext ctx = null; + DiskInterface disk = null; + int searchId = -1; + + try + { + + // Access the disk interface + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Allocate a search slot for the new search + + searchId = m_sess.allocateSearchSlot(); + if (searchId == -1) + { + + // Failed to allocate a slot for the new search + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoResourcesAvailable, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Start trans search [" + searchId + "] - " + srchPath + ", attr=0x" + + Integer.toHexString(srchAttr) + ", maxFiles=" + maxFiles + ", infoLevel=" + infoLevl + + ", flags=0x" + Integer.toHexString(srchFlag)); + + // Start a new search + + ctx = disk.startSearch(m_sess, conn, srchPath, srchAttr); + if (ctx != null) + { + + // Store details of the search in the context + + ctx.setTreeId(treeId); + ctx.setMaximumFiles(maxFiles); + } + else + { + + // Failed to start the search, return a no more files error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Save the search context + + m_sess.setSearchContext(searchId, ctx); + + // Create the reply transact buffer + + SrvTransactBuffer replyBuf = new SrvTransactBuffer(tbuf); + DataBuffer dataBuf = replyBuf.getDataBuffer(); + + // Determine the maximum return data length + + int maxLen = replyBuf.getReturnDataLimit(); + + // Check if resume keys are required + + boolean resumeReq = (srchFlag & FindFirstNext.ReturnResumeKey) != 0 ? true : false; + + // Loop until we have filled the return buffer or there are no more files to return + + int fileCnt = 0; + int packLen = 0; + int lastNameOff = 0; + + boolean pktDone = false; + boolean searchDone = false; + + FileInfo info = new FileInfo(); + + while (pktDone == false && fileCnt < maxFiles) + { + + // Get file information from the search + + if (ctx.nextFileInfo(info) == false) + { + + // No more files + + pktDone = true; + searchDone = true; + } + + // Check if the file information will fit into the return buffer + + else if (FindInfoPacker.calcInfoSize(info, infoLevl, false, true) <= maxLen) + { + + // Pack a dummy resume key, if required + + if (resumeReq) + { + dataBuf.putZeros(4); + maxLen -= 4; + } + + // Save the offset to the last file information structure + + lastNameOff = dataBuf.getPosition(); + + // Pack the file information + + packLen = FindInfoPacker.packInfo(info, dataBuf, infoLevl, tbuf.isUnicode()); + + // Update the file count for this packet + + fileCnt++; + + // Recalculate the remaining buffer space + + maxLen -= packLen; + } + else + { + + // Set the search restart point + + ctx.restartAt(info); + + // No more buffer space + + pktDone = true; + } + } + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(searchId); + paramBuf.putShort(fileCnt); + paramBuf.putShort(ctx.hasMoreFiles() ? 0 : 1); + paramBuf.putShort(0); + paramBuf.putShort(lastNameOff); + + // Send the transaction response + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(outPkt.getBuffer()); + tpkt.doTransactionResponse(m_sess, replyBuf); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search [" + searchId + "] Returned " + fileCnt + " files, moreFiles=" + + ctx.hasMoreFiles()); + + // Check if the search is complete + + if (searchDone == true) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("End start search [" + searchId + "] (Search complete)"); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + } + } + catch (FileNotFoundException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Search path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + // m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + } + catch (UnsupportedInfoLevelException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + } + + /** + * Process a transact2 file search continue request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2FindNext(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the search parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int searchId = paramBuf.getShort(); + int maxFiles = paramBuf.getShort(); + int infoLevl = paramBuf.getShort(); + int reskey = paramBuf.getInt(); + int srchFlag = paramBuf.getShort(); + + String resumeName = paramBuf.getString(tbuf.isUnicode()); + + // Access the shared device disk interface + + SearchContext ctx = null; + DiskInterface disk = null; + + try + { + + // Access the disk interface + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Retrieve the search context + + ctx = m_sess.getSearchContext(searchId); + if (ctx == null) + { + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search context null - [" + searchId + "]"); + + // Invalid search handle + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Continue search [" + searchId + "] - " + resumeName + ", maxFiles=" + maxFiles + + ", infoLevel=" + infoLevl + ", flags=0x" + Integer.toHexString(srchFlag)); + + // Create the reply transaction buffer + + SrvTransactBuffer replyBuf = new SrvTransactBuffer(tbuf); + DataBuffer dataBuf = replyBuf.getDataBuffer(); + + // Determine the maximum return data length + + int maxLen = replyBuf.getReturnDataLimit(); + + // Check if resume keys are required + + boolean resumeReq = (srchFlag & FindFirstNext.ReturnResumeKey) != 0 ? true : false; + + // Loop until we have filled the return buffer or there are no more files to return + + int fileCnt = 0; + int packLen = 0; + int lastNameOff = 0; + + boolean pktDone = false; + boolean searchDone = false; + + FileInfo info = new FileInfo(); + + while (pktDone == false && fileCnt < maxFiles) + { + + // Get file information from the search + + if (ctx.nextFileInfo(info) == false) + { + + // No more files + + pktDone = true; + searchDone = true; + } + + // Check if the file information will fit into the return buffer + + else if (FindInfoPacker.calcInfoSize(info, infoLevl, false, true) <= maxLen) + { + + // Pack a dummy resume key, if required + + if (resumeReq) + dataBuf.putZeros(4); + + // Save the offset to the last file information structure + + lastNameOff = dataBuf.getPosition(); + + // Pack the file information + + packLen = FindInfoPacker.packInfo(info, dataBuf, infoLevl, tbuf.isUnicode()); + + // Update the file count for this packet + + fileCnt++; + + // Recalculate the remaining buffer space + + maxLen -= packLen; + } + else + { + + // Set the search restart point + + ctx.restartAt(info); + + // No more buffer space + + pktDone = true; + } + } + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(fileCnt); + paramBuf.putShort(ctx.hasMoreFiles() ? 0 : 1); + paramBuf.putShort(0); + paramBuf.putShort(lastNameOff); + + // Send the transaction response + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(outPkt.getBuffer()); + tpkt.doTransactionResponse(m_sess, replyBuf); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search [" + searchId + "] Returned " + fileCnt + " files, moreFiles=" + + ctx.hasMoreFiles()); + + // Check if the search is complete + + if (searchDone == true) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("End start search [" + searchId + "] (Search complete)"); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + } + } + catch (FileNotFoundException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Search path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + // m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + } + catch (UnsupportedInfoLevelException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + } + + /** + * Process a transact2 file system query request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2QueryFileSys(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) + throws java.io.IOException, SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the query file system required information level + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevl = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Query File System Info - level = 0x" + Integer.toHexString(infoLevl)); + + // Access the shared device disk interface + + try + { + + // Access the disk interface and context + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the disk information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = DataPacker.longwordAlign(outPkt.getByteOffset()); + int dataPos = prmPos; // no parameters returned + + // Create a data buffer using the SMB packet. The response should always fit into a + // single + // reply packet. + + DataBuffer replyBuf = new DataBuffer(buf, dataPos, buf.length - dataPos); + + // Determine the information level requested + + SrvDiskInfo diskInfo = null; + VolumeInfo volInfo = null; + + switch (infoLevl) + { + + // Standard disk information + + case DiskInfoPacker.InfoStandard: + + // Get the disk information + + diskInfo = getDiskInformation(disk, diskCtx); + + // Pack the disk information into the return data packet + + DiskInfoPacker.packStandardInfo(diskInfo, replyBuf); + break; + + // Volume label information + + case DiskInfoPacker.InfoVolume: + + // Get the volume label information + + volInfo = getVolumeInformation(disk, diskCtx); + + // Pack the volume label information + + DiskInfoPacker.packVolumeInfo(volInfo, replyBuf, tbuf.isUnicode()); + break; + + // Full volume information + + case DiskInfoPacker.InfoFsVolume: + + // Get the volume information + + volInfo = getVolumeInformation(disk, diskCtx); + + // Pack the volume information + + DiskInfoPacker.packFsVolumeInformation(volInfo, replyBuf, tbuf.isUnicode()); + break; + + // Filesystem size information + + case DiskInfoPacker.InfoFsSize: + + // Get the disk information + + diskInfo = getDiskInformation(disk, diskCtx); + + // Pack the disk information into the return data packet + + DiskInfoPacker.packFsSizeInformation(diskInfo, replyBuf); + break; + + // Filesystem device information + + case DiskInfoPacker.InfoFsDevice: + DiskInfoPacker.packFsDevice(0, 0, replyBuf); + break; + + // Filesystem attribute information + + case DiskInfoPacker.InfoFsAttribute: + DiskInfoPacker.packFsAttribute(0, 255, "JLAN", tbuf.isUnicode(), replyBuf); + break; + } + + // Check if any data was packed, if not then the information level is not supported + + if (replyBuf.getPosition() == dataPos) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + int dataLen = replyBuf.getLength(); + SMBSrvTransPacket.initTransactReply(outPkt, 0, prmPos, dataLen, dataPos); + outPkt.setByteCount(replyBuf.getPosition() - outPkt.getByteOffset()); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + } + + /** + * Process a transact2 query path information request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2QueryPath(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the query path information level and file/directory name + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevl = paramBuf.getShort(); + paramBuf.skipBytes(4); + + String path = paramBuf.getString(tbuf.isUnicode()); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Query Path - level = 0x" + Integer.toHexString(infoLevl) + ", path = " + path); + + // Access the shared device disk interface + + try + { + + // Access the disk interface + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the file information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = DataPacker.longwordAlign(outPkt.getByteOffset()); + int dataPos = prmPos; // no parameters returned + + // Create a data buffer using the SMB packet. The response should always fit into a + // single + // reply packet. + + DataBuffer replyBuf = new DataBuffer(buf, dataPos, buf.length - dataPos); + + // Get the file information + + FileInfo fileInfo = disk.getFileInformation(m_sess, conn, path); + + if (fileInfo == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.NTErr); + return; + } + + // Pack the file information into the return data packet + + int dataLen = QueryInfoPacker.packInfo(fileInfo, replyBuf, infoLevl, true); + + // Check if any data was packed, if not then the information level is not supported + + if (dataLen == 0) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + + SMBSrvTransPacket.initTransactReply(outPkt, 0, prmPos, dataLen, dataPos); + outPkt.setByteCount(replyBuf.getPosition() - outPkt.getByteOffset()); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + } + catch (FileNotFoundException ex) + { + + // Requested file does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.NTErr); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + catch (UnsupportedInfoLevelException ex) + { + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.NTErr); + return; + } + } + + /** + * Process the SMB tree connect request. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + * @exception TooManyConnectionsException Too many concurrent connections on this session. + */ + + protected void procTreeConnectAndX(SMBSrvPacket outPkt) throws SMBSrvException, TooManyConnectionsException, + java.io.IOException + { + + // Check that the received packet looks like a valid tree connect request + + if (m_smbPkt.checkPacketIsValid(4, 3) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Extract the parameters + + int flags = m_smbPkt.getParameter(2); + int pwdLen = m_smbPkt.getParameter(3); + + // Get the data bytes position and length + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Extract the password string + + String pwd = null; + + if (pwdLen > 0) + { + pwd = new String(buf, dataPos, pwdLen); + dataPos += pwdLen; + dataLen -= pwdLen; + } + + // Extract the requested share name, as a UNC path + + String uncPath = DataPacker.getString(buf, dataPos, dataLen); + if (uncPath == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Extract the service type string + + dataPos += uncPath.length() + 1; // null terminated + dataLen -= uncPath.length() + 1; // null terminated + + String service = DataPacker.getString(buf, dataPos, dataLen); + if (service == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Convert the service type to a shared device type, client may specify '?????' in which + // case we ignore the error. + + int servType = ShareType.ServiceAsType(service); + if (servType == ShareType.UNKNOWN && service.compareTo("?????") != 0) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("Tree Connect AndX - " + uncPath + ", " + service); + + // Parse the requested share name + + PCShare share = null; + + try + { + share = new PCShare(uncPath); + } + catch (InvalidUNCPathException ex) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Map the IPC$ share to the admin pipe type + + if (servType == ShareType.NAMEDPIPE && share.getShareName().compareTo("IPC$") == 0) + servType = ShareType.ADMINPIPE; + + // Find the requested shared device + + SharedDevice shareDev = null; + + try + { + + // Get/create the shared device + + shareDev = m_sess.getSMBServer().findShare(share.getNodeName(), share.getShareName(), servType, + getSession(), true); + } + catch (InvalidUserException ex) + { + + // Return a logon failure status + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (Exception ex) + { + + // Return a general status, bad network name + + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidNetworkName, SMBStatus.ErrSrv); + return; + } + + // Check if the share is valid + + if (shareDev == null || (servType != ShareType.UNKNOWN && shareDev.getType() != servType)) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Authenticate the share connection depending upon the security mode the server is running + // under + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + int filePerm = FileAccess.Writeable; + + if (auth != null) + { + + // Validate the share connection + + filePerm = auth.authenticateShareConnect(m_sess.getClientInformation(), shareDev, pwd, m_sess); + if (filePerm < 0) + { + + // Invalid share connection request + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + } + + // Allocate a tree id for the new connection + + int treeId = m_sess.addConnection(shareDev); + outPkt.setTreeId(treeId); + + // Set the file permission that this user has been granted for this share + + TreeConnection tree = m_sess.findConnection(treeId); + tree.setPermission(filePerm); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("Tree Connect AndX - Allocated Tree Id = " + treeId + ", Permission = " + + FileAccess.asString(filePerm)); + + // Build the tree connect response + + outPkt.setParameterCount(3); + outPkt.setAndXCommand(0xFF); // no chained reply + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 0); + + // Pack the service type + + int pos = outPkt.getByteOffset(); + pos = DataPacker.putString(ShareType.TypeAsService(shareDev.getType()), buf, pos, true); + outPkt.setByteCount(pos - outPkt.getByteOffset()); + + // Send the response + + m_sess.sendResponseSMB(outPkt); + + // Inform the driver that a connection has been opened + + if (tree.getInterface() != null) + tree.getInterface().treeOpened(m_sess, tree); + } + + /** + * Process the file write request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procWriteAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid write andX request + + if (m_smbPkt.checkPacketIsValid(12, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVInvalidTID, SMBStatus.ErrSrv); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + + // Extract the write file parameters + + int fid = m_smbPkt.getParameter(2); + int offset = m_smbPkt.getParameterLong(3); + int dataLen = m_smbPkt.getParameter(10); + int dataPos = m_smbPkt.getParameter(11) + RFCNetBIOSProtocol.HEADER_LEN; + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write AndX [" + netFile.getFileId() + "] : Size=" + dataLen + " ,Pos=" + offset); + + // Write data to the file + + byte[] buf = m_smbPkt.getBuffer(); + int wrtlen = 0; + + // Access the disk interface and write to the file + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Write to the file + + wrtlen = disk.writeFile(m_sess, conn, netFile, buf, dataPos, dataLen, offset); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + logger.error("File Write Error [" + netFile.getFileId() + "] : ", ex); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + + // Return the count of bytes actually written + + outPkt.setParameterCount(6); + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); + outPkt.setParameter(2, wrtlen); + outPkt.setParameter(3, 0); // remaining byte count for pipes only + outPkt.setParameter(4, 0); // reserved + outPkt.setParameter(5, 0); // " + outPkt.setByteCount(0); + + // Send the write response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * runProtocol method comment. + */ + public boolean runProtocol() throws java.io.IOException, SMBSrvException, TooManyConnectionsException + { + + // Check if the SMB packet is initialized + + if (m_smbPkt == null) + m_smbPkt = m_sess.getReceivePacket(); + + // Check if the received packet has a valid SMB signature + + if (m_smbPkt.checkPacketSignature() == false) + throw new IOException("Invalid SMB signature"); + + // Determine if the request has a chained command, if so then we will copy the incoming + // request so that + // a chained reply can be built. + + SMBSrvPacket outPkt = m_smbPkt; + boolean chainedCmd = hasChainedCommand(m_smbPkt); + + if (chainedCmd) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_STATE)) + logger.debug("AndX Command = 0x" + Integer.toHexString(m_smbPkt.getAndXCommand())); + + // Copy the request packet into a new packet for the reply + + outPkt = new SMBSrvPacket(m_smbPkt); + } + + // Reset the byte unpack offset + + m_smbPkt.resetBytePointer(); + + // Determine the SMB command type + + boolean handledOK = true; + + switch (m_smbPkt.getCommand()) + { + + // Session setup + + case PacketType.SessionSetupAndX: + procSessionSetup(outPkt); + break; + + // Tree connect + + case PacketType.TreeConnectAndX: + procTreeConnectAndX(outPkt); + break; + + // Transaction2 + + case PacketType.Transaction2: + case PacketType.Transaction: + procTransact2(outPkt); + break; + + // Transaction/transaction2 secondary + + case PacketType.TransactionSecond: + case PacketType.Transaction2Second: + procTransact2Secondary(outPkt); + break; + + // Close a search started via the FindFirst transaction2 command + + case PacketType.FindClose2: + procFindClose(outPkt); + break; + + // Open a file + + case PacketType.OpenAndX: + procOpenAndX(outPkt); + break; + + // Read a file + + case PacketType.ReadAndX: + procReadAndX(outPkt); + break; + + // Write to a file + + case PacketType.WriteAndX: + procWriteAndX(outPkt); + break; + + // Tree disconnect + + case PacketType.TreeDisconnect: + procTreeDisconnect(outPkt); + break; + + // Lock/unlock regions of a file + + case PacketType.LockingAndX: + procLockingAndX(outPkt); + break; + + // Logoff a user + + case PacketType.LogoffAndX: + procLogoffAndX(outPkt); + break; + + // Tree connection (without AndX batching) + + case PacketType.TreeConnect: + super.runProtocol(); + break; + + // Rename file + + case PacketType.RenameFile: + procRenameFile(outPkt); + break; + + // Echo request + + case PacketType.Echo: + super.procEcho(outPkt); + break; + + // Default + + default: + + // Get the tree connection details, if it is a disk or printer type connection then pass + // the request to the + // core protocol handler + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = null; + if (treeId != -1) + conn = m_sess.findConnection(treeId); + + if (conn != null) + { + + // Check if this is a disk or print connection, if so then send the request to the + // core protocol handler + + if (conn.getSharedDevice().getType() == ShareType.DISK + || conn.getSharedDevice().getType() == ShareType.PRINTER) + { + + // Chain to the core protocol handler + + handledOK = super.runProtocol(); + } + else if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Send the request to IPC$ remote admin handler + + IPCHandler.processIPCRequest(m_sess, outPkt); + handledOK = true; + } + } + break; + } + + // Return the handled status + + return handledOK; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/NTParameterPacker.java b/source/java/org/alfresco/filesys/smb/server/NTParameterPacker.java new file mode 100644 index 0000000000..1d2e2311ee --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/NTParameterPacker.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.util.DataPacker; + +/** + * NT Dialect Parameter Packer Class + *

    + * The NT SMB dialect uses parameters that are not always word/longword aligned. + */ +class NTParameterPacker +{ + + // Buffer and current offset + + private byte[] m_buf; + private int m_pos; + + /** + * Class constructor + * + * @param buf byte[] + */ + public NTParameterPacker(byte[] buf) + { + m_buf = buf; + m_pos = SMBSrvPacket.PARAMWORDS; + } + + /** + * Class constructor + * + * @param buf byte[] + * @param pos int + */ + public NTParameterPacker(byte[] buf, int pos) + { + m_buf = buf; + m_pos = pos; + } + + /** + * Pack a byte (8 bit) value + * + * @param val byte + */ + public final void packByte(byte val) + { + m_buf[m_pos++] = val; + } + + /** + * Pack a byte (8 bit) value + * + * @param val int + */ + public final void packByte(int val) + { + m_buf[m_pos++] = (byte) val; + } + + /** + * Pack a word (16 bit) value + * + * @param val int + */ + public final void packWord(int val) + { + DataPacker.putIntelShort(val, m_buf, m_pos); + m_pos += 2; + } + + /** + * Pack an integer (32 bit) value + * + * @param val int + */ + public final void packInt(int val) + { + DataPacker.putIntelInt(val, m_buf, m_pos); + m_pos += 4; + } + + /** + * Pack a long (64 bit) value + * + * @param val long + */ + public final void packLong(long val) + { + DataPacker.putIntelLong(val, m_buf, m_pos); + m_pos += 8; + } + + /** + * Return the current buffer position + * + * @return int + */ + public final int getPosition() + { + return m_pos; + } + + /** + * Return the buffer + * + * @return byte[] + */ + public final byte[] getBuffer() + { + return m_buf; + } + + /** + * Unpack a byte value + * + * @return int + */ + public final int unpackByte() + { + return (int) m_buf[m_pos++]; + } + + /** + * Unpack a word (16 bit) value + * + * @return int + */ + public final int unpackWord() + { + int val = DataPacker.getIntelShort(m_buf, m_pos); + m_pos += 2; + return val; + } + + /** + * Unpack an integer (32 bit) value + * + * @return int + */ + public final int unpackInt() + { + int val = DataPacker.getIntelInt(m_buf, m_pos); + m_pos += 4; + return val; + } + + /** + * Unpack a long (64 bit) value + * + * @return int + */ + public final long unpackLong() + { + long val = DataPacker.getIntelLong(m_buf, m_pos); + m_pos += 8; + return val; + } + + /** + * Reset the parameter packer/reader to use the new buffer/offset + * + * @param buf byte[] + * @param off int + */ + public final void reset(byte[] buf, int pos) + { + m_buf = buf; + m_pos = pos; + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java b/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java new file mode 100644 index 0000000000..725299586e --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java @@ -0,0 +1,7007 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.alfresco.filesys.locking.FileLock; +import org.alfresco.filesys.locking.LockConflictException; +import org.alfresco.filesys.locking.NotLockedException; +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.auth.InvalidUserException; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.auth.acl.AccessControl; +import org.alfresco.filesys.server.auth.acl.AccessControlManager; +import org.alfresco.filesys.server.core.InvalidDeviceInterfaceException; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.filesys.AccessDeniedException; +import org.alfresco.filesys.server.filesys.DirectoryNotEmptyException; +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskFullException; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.FileAccess; +import org.alfresco.filesys.server.filesys.FileAction; +import org.alfresco.filesys.server.filesys.FileAttribute; +import org.alfresco.filesys.server.filesys.FileExistsException; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.filesys.server.filesys.FileOfflineException; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.FileSharingException; +import org.alfresco.filesys.server.filesys.FileStatus; +import org.alfresco.filesys.server.filesys.FileSystem; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.NotifyChange; +import org.alfresco.filesys.server.filesys.PathNotFoundException; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.filesys.server.filesys.SrvDiskInfo; +import org.alfresco.filesys.server.filesys.TooManyConnectionsException; +import org.alfresco.filesys.server.filesys.TooManyFilesException; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.server.filesys.UnsupportedInfoLevelException; +import org.alfresco.filesys.server.filesys.VolumeInfo; +import org.alfresco.filesys.server.locking.FileLockingInterface; +import org.alfresco.filesys.server.locking.LockManager; +import org.alfresco.filesys.smb.DataType; +import org.alfresco.filesys.smb.FileInfoLevel; +import org.alfresco.filesys.smb.FindFirstNext; +import org.alfresco.filesys.smb.InvalidUNCPathException; +import org.alfresco.filesys.smb.LockingAndX; +import org.alfresco.filesys.smb.NTIOCtl; +import org.alfresco.filesys.smb.NTTime; +import org.alfresco.filesys.smb.PCShare; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBDate; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.WinNT; +import org.alfresco.filesys.smb.server.notify.NotifyChangeEventList; +import org.alfresco.filesys.smb.server.notify.NotifyChangeHandler; +import org.alfresco.filesys.smb.server.notify.NotifyRequest; +import org.alfresco.filesys.smb.server.ntfs.NTFSStreamsInterface; +import org.alfresco.filesys.smb.server.ntfs.StreamInfoList; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.HexDump; +import org.alfresco.filesys.util.WildCard; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * NT SMB Protocol Handler Class + *

    + * The NT protocol handler processes the additional SMBs that were added to the protocol in the NT + * SMB dialect. + */ +public class NTProtocolHandler extends CoreProtocolHandler +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Constants + // + // Flag to enable returning of '.' and '..' directory information in FindFirst request + + public static final boolean ReturnDotFiles = true; + + // Flag to enable faking of oplock requests when opening files + + public static final boolean FakeOpLocks = false; + + // Number of write requests per file to report file size change notifications + + public static final int FileSizeChangeRate = 10; + + // Security descriptor to allow Everyone access, returned by the QuerySecurityDescrptor NT + // transaction + // when NTFS streams are enabled for a virtual filesystem. + + private static byte[] _sdEveryOne = { 0x01, 0x00, 0x04, (byte) 0x80, 0x14, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x1c, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, + (byte) 0xff, 0x01, 0x1f, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 + }; + + /** + * Class constructor. + */ + protected NTProtocolHandler() + { + super(); + } + + /** + * Class constructor + * + * @param sess SMBSrvSession + */ + protected NTProtocolHandler(SMBSrvSession sess) + { + super(sess); + } + + /** + * Return the protocol name + * + * @return String + */ + public String getName() + { + return "NT"; + } + + /** + * Run the NT SMB protocol handler to process the received SMB packet + * + * @exception IOException + * @exception SMBSrvException + * @exception TooManyConnectionsException + */ + public boolean runProtocol() throws java.io.IOException, SMBSrvException, TooManyConnectionsException + { + + // Check if the SMB packet is initialized + + if (m_smbPkt == null) + m_smbPkt = m_sess.getReceivePacket(); + + // Check if the received packet has a valid SMB signature + + if (m_smbPkt.checkPacketSignature() == false) + throw new IOException("Invalid SMB signature"); + + // Determine if the request has a chained command, if so then we will copy the incoming + // request so that + // a chained reply can be built. + + SMBSrvPacket outPkt = m_smbPkt; + boolean chainedCmd = hasChainedCommand(m_smbPkt); + + if (chainedCmd) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_STATE)) + logger.debug("AndX Command = 0x" + Integer.toHexString(m_smbPkt.getAndXCommand())); + + // Copy the request packet into a new packet for the reply + + outPkt = new SMBSrvPacket(m_smbPkt, m_smbPkt.getPacketLength()); + } + + // Reset the byte unpack offset + + m_smbPkt.resetBytePointer(); + + // Set the process id from the received packet, this can change for the same session and + // needs to be set + // for lock ownership checking + + m_sess.setProcessId(m_smbPkt.getProcessId()); + + // Determine the SMB command type + + boolean handledOK = true; + + switch (m_smbPkt.getCommand()) + { + + // NT Session setup + + case PacketType.SessionSetupAndX: + procSessionSetup(outPkt); + break; + + // Tree connect + + case PacketType.TreeConnectAndX: + procTreeConnectAndX(outPkt); + break; + + // Transaction/transaction2 + + case PacketType.Transaction: + case PacketType.Transaction2: + procTransact2(outPkt); + break; + + // Transaction/transaction2 secondary + + case PacketType.TransactionSecond: + case PacketType.Transaction2Second: + procTransact2Secondary(outPkt); + break; + + // Close a search started via the FindFirst transaction2 command + + case PacketType.FindClose2: + procFindClose(outPkt); + break; + + // Open a file + + case PacketType.OpenAndX: + procOpenAndX(outPkt); + break; + + // Close a file + + case PacketType.CloseFile: + procCloseFile(outPkt); + break; + + // Read a file + + case PacketType.ReadAndX: + procReadAndX(outPkt); + break; + + // Write to a file + + case PacketType.WriteAndX: + procWriteAndX(outPkt); + break; + + // Rename file + + case PacketType.RenameFile: + procRenameFile(outPkt); + break; + + // Delete file + + case PacketType.DeleteFile: + procDeleteFile(outPkt); + break; + + // Delete directory + + case PacketType.DeleteDirectory: + procDeleteDirectory(outPkt); + break; + + // Tree disconnect + + case PacketType.TreeDisconnect: + procTreeDisconnect(outPkt); + break; + + // Lock/unlock regions of a file + + case PacketType.LockingAndX: + procLockingAndX(outPkt); + break; + + // Logoff a user + + case PacketType.LogoffAndX: + procLogoffAndX(outPkt); + break; + + // NT Create/open file + + case PacketType.NTCreateAndX: + procNTCreateAndX(outPkt); + break; + + // Tree connection (without AndX batching) + + case PacketType.TreeConnect: + super.runProtocol(); + break; + + // NT cancel + + case PacketType.NTCancel: + procNTCancel(outPkt); + break; + + // NT transaction + + case PacketType.NTTransact: + procNTTransaction(outPkt); + break; + + // NT transaction secondary + + case PacketType.NTTransactSecond: + procNTTransactionSecondary(outPkt); + break; + + // Echo request + + case PacketType.Echo: + super.procEcho(outPkt); + break; + + // Default + + default: + + // Get the tree connection details, if it is a disk or printer type connection then pass + // the request to the + // core protocol handler + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = null; + if (treeId != -1) + conn = m_sess.findConnection(treeId); + + if (conn != null) + { + + // Check if this is a disk or print connection, if so then send the request to the + // core protocol handler + + if (conn.getSharedDevice().getType() == ShareType.DISK + || conn.getSharedDevice().getType() == ShareType.PRINTER) + { + + // Chain to the core protocol handler + + handledOK = super.runProtocol(); + } + else if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Send the request to IPC$ remote admin handler + + IPCHandler.processIPCRequest(m_sess, outPkt); + handledOK = true; + } + } + break; + } + + // Return the handled status + + return handledOK; + } + + /** + * Process the NT SMB session setup request. + * + * @param outPkt Response SMB packet. + */ + protected void procSessionSetup(SMBSrvPacket outPkt) throws SMBSrvException, IOException, + TooManyConnectionsException + { + + // Check that the received packet looks like a valid NT session setup andX request + + if (m_smbPkt.checkPacketIsValid(13, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Extract the session details + + int maxBufSize = m_smbPkt.getParameter(2); + int maxMpx = m_smbPkt.getParameter(3); + int vcNum = m_smbPkt.getParameter(4); + int sessKey = m_smbPkt.getParameterLong(5); + int ascPwdLen = m_smbPkt.getParameter(7); + int uniPwdLen = m_smbPkt.getParameter(8); + int capabs = m_smbPkt.getParameter(11); + + // Extract the client details from the session setup request + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + byte[] buf = m_smbPkt.getBuffer(); + + // Determine if ASCII or unicode strings are being used + + boolean isUni = m_smbPkt.isUnicode(); + + // Extract the password strings + + byte[] ascPwd = m_smbPkt.unpackBytes(ascPwdLen); + byte[] uniPwd = m_smbPkt.unpackBytes(uniPwdLen); + + // Extract the user name string + + String user = m_smbPkt.unpackString(isUni); + + if (user == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Extract the clients primary domain name string + + String domain = ""; + + if (m_smbPkt.hasMoreData()) + { + + // Extract the callers domain name + + domain = m_smbPkt.unpackString(isUni); + + if (domain == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + } + + // Extract the clients native operating system + + String clientOS = ""; + + if (m_smbPkt.hasMoreData()) + { + + // Extract the callers operating system name + + clientOS = m_smbPkt.unpackString(isUni); + + if (clientOS == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + } + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + { + logger.debug("NT Session setup from user=" + user + ", password=" + + (uniPwd != null ? HexDump.hexString(uniPwd) : "none") + ", ANSIpwd=" + + (ascPwd != null ? HexDump.hexString(ascPwd) : "none") + ", domain=" + domain + ", os=" + clientOS + + ", VC=" + vcNum + ", maxBuf=" + maxBufSize + ", maxMpx=" + maxMpx); + logger.debug(" MID=" + m_smbPkt.getMultiplexId() + ", UID=" + m_smbPkt.getUserId() + ", PID=" + + m_smbPkt.getProcessId()); + } + + // Store the client maximum buffer size, maximum multiplexed requests count and client + // capability flags + + m_sess.setClientMaximumBufferSize(maxBufSize); + m_sess.setClientMaximumMultiplex(maxMpx); + m_sess.setClientCapabilities(capabs); + + // Create the client information and store in the session + + ClientInfo client = new ClientInfo(user, uniPwd); + client.setANSIPassword(ascPwd); + client.setDomain(domain); + client.setOperatingSystem(clientOS); + + if (m_sess.hasRemoteAddress()) + client.setClientAddress(m_sess.getRemoteAddress().getHostAddress()); + + // Check if this is a null session logon + + if (user.length() == 0 && domain.length() == 0 && uniPwdLen == 0 && ascPwdLen == 1) + client.setLogonType(ClientInfo.LogonNull); + + // Authenticate the user, if the server is using user mode security + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + boolean isGuest = false; + + if (auth != null && auth.getAccessMode() == SrvAuthenticator.USER_MODE) + { + + // Validate the user + + int sts = auth.authenticateUser(client, m_sess, SrvAuthenticator.NTLM1); + + if (sts > 0 && (sts & SrvAuthenticator.AUTH_GUEST) != 0) + { + + // Guest logon + + isGuest = true; + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + logger.debug("User " + user + ", logged on as guest"); + } + else if (sts != SrvAuthenticator.AUTH_ALLOW) + { + + // Check if the session already has valid client details and the new client details + // have null username/password + // values + + if (getSession().getClientInformation() != null && client.getUserName().length() == 0) + { + + // Use the existing client information details + + client = getSession().getClientInformation(); + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + logger.debug("Null client information, reusing existing client=" + client); + } + else + { + + // Invalid user, reject the session setup request + + m_sess.sendErrorResponseSMB(SMBStatus.NTLogonFailure, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + logger.debug("User " + user + ", access denied"); + return; + } + } + else if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + { + + // DEBUG + + logger.debug("User " + user + " logged on " + + (client != null ? " (type " + client.getLogonTypeString() + ")" : "")); + } + } + + // Update the client information if not already set + + if (getSession().getClientInformation() == null + || getSession().getClientInformation().getUserName().length() == 0) + { + + // Set the client details for the session + + getSession().setClientInformation(client); + } + + // Set the guest flag for the client, indicate that the session is logged on + + client.setGuest(isGuest); + getSession().setLoggedOn(true); + + // Build the session setup response SMB + + outPkt.setParameterCount(3); + outPkt.setParameter(0, 0); // No chained response + outPkt.setParameter(1, 0); // Offset to chained response + outPkt.setParameter(2, isGuest ? 1 : 0); + outPkt.setByteCount(0); + + outPkt.setTreeId(0); + outPkt.setUserId(0); + + // Set the various flags + + int flags = outPkt.getFlags(); + flags &= ~SMBSrvPacket.FLG_CASELESS; + outPkt.setFlags(flags); + + int flags2 = SMBSrvPacket.FLG2_LONGFILENAMES; + if (isUni) + flags2 += SMBSrvPacket.FLG2_UNICODE; + outPkt.setFlags2(flags2); + + // Pack the OS, dialect and domain name strings. + + int pos = outPkt.getByteOffset(); + buf = outPkt.getBuffer(); + + if (isUni) + pos = DataPacker.wordAlign(pos); + + pos = DataPacker.putString("Java", buf, pos, true, isUni); + pos = DataPacker.putString("Alfresco CIFS Server " + m_sess.getServer().isVersion(), buf, pos, true, isUni); + pos = DataPacker.putString(m_sess.getServer().getConfiguration().getDomainName(), buf, pos, true, isUni); + + outPkt.setByteCount(pos - outPkt.getByteOffset()); + + // Check if there is a chained command, or commands + + if (m_smbPkt.hasAndXCommand() && dataPos < m_smbPkt.getReceivedLength()) + { + + // Process any chained commands, AndX + + pos = procAndXCommands(outPkt); + pos -= RFCNetBIOSProtocol.HEADER_LEN; + } + else + { + + // Indicate that there are no chained replies + + outPkt.setAndXCommand(SMBSrvPacket.NO_ANDX_CMD); + } + + // Send the negotiate response + + m_sess.sendResponseSMB(outPkt, pos); + + // Update the session state + + m_sess.setState(SMBSrvSessionState.SMBSESSION); + + // Notify listeners that a user has logged onto the session + + m_sess.getSMBServer().sessionLoggedOn(m_sess); + } + + /** + * Process the chained SMB commands (AndX). + * + * @param outPkt Reply packet. + * @return New offset to the end of the reply packet + */ + protected final int procAndXCommands(SMBSrvPacket outPkt) + { + + // Use the byte offset plus length to calculate the current output packet end position + + return procAndXCommands(outPkt, outPkt.getByteOffset() + outPkt.getByteCount(), null); + } + + /** + * Process the chained SMB commands (AndX). + * + * @param outPkt Reply packet. + * @param endPos Current end of packet position + * @param file Current file , or null if no file context in chain + * @return New offset to the end of the reply packet + */ + protected final int procAndXCommands(SMBSrvPacket outPkt, int endPos, NetworkFile file) + { + + // Get the chained command and command block offset + + int andxCmd = m_smbPkt.getAndXCommand(); + int andxOff = m_smbPkt.getParameter(1) + RFCNetBIOSProtocol.HEADER_LEN; + + // Set the initial chained command and offset + + outPkt.setAndXCommand(andxCmd); + outPkt.setParameter(1, andxOff - RFCNetBIOSProtocol.HEADER_LEN); + + // Pointer to the last parameter block, starts with the main command parameter block + + int paramBlk = SMBSrvPacket.WORDCNT; + + // Get the current end of the reply packet offset + + int endOfPkt = endPos; + boolean andxErr = false; + + while (andxCmd != SMBSrvPacket.NO_ANDX_CMD && andxErr == false) + { + + // Determine the chained command type + + int prevEndOfPkt = endOfPkt; + boolean endOfChain = false; + + switch (andxCmd) + { + + // Tree connect + + case PacketType.TreeConnectAndX: + endOfPkt = procChainedTreeConnectAndX(andxOff, outPkt, endOfPkt); + break; + + // Close file + + case PacketType.CloseFile: + endOfPkt = procChainedClose(andxOff, outPkt, endOfPkt); + endOfChain = true; + break; + + // Read file + + case PacketType.ReadAndX: + endOfPkt = procChainedReadAndX(andxOff, outPkt, endOfPkt, file); + break; + + // Chained command was not handled + + default: + break; + } + + // Set the next chained command details in the current parameter block + + outPkt.setAndXCommand(paramBlk, andxCmd); + outPkt.setAndXParameter(paramBlk, 1, prevEndOfPkt - RFCNetBIOSProtocol.HEADER_LEN); + + // Check if the end of chain has been reached, if not then look for the next + // chained command in the request. End of chain might be set if the current command + // is not an AndX SMB command. + + if (endOfChain == false) + { + + // Advance to the next chained command block + + andxCmd = m_smbPkt.getAndXParameter(andxOff, 0) & 0x00FF; + andxOff = m_smbPkt.getAndXParameter(andxOff, 1); + + // Advance the current parameter block + + paramBlk = prevEndOfPkt; + } + else + { + + // Indicate that the end of the command chain has been reached + + andxCmd = SMBSrvPacket.NO_ANDX_CMD; + } + + // Check if the chained command has generated an error status + + if (outPkt.getErrorCode() != SMBStatus.Success) + andxErr = true; + } + + // Return the offset to the end of the reply packet + + return endOfPkt; + } + + /** + * Process a chained tree connect request. + * + * @return New end of reply offset. + * @param cmdOff int Offset to the chained command within the request packet. + * @param outPkt SMBSrvPacket Reply packet. + * @param endOff int Offset to the current end of the reply packet. + */ + protected final int procChainedTreeConnectAndX(int cmdOff, SMBSrvPacket outPkt, int endOff) + { + + // Extract the parameters + + int flags = m_smbPkt.getAndXParameter(cmdOff, 2); + int pwdLen = m_smbPkt.getAndXParameter(cmdOff, 3); + + // Reset the byte pointer for data unpacking + + m_smbPkt.setBytePointer(m_smbPkt.getAndXByteOffset(cmdOff), m_smbPkt.getAndXByteCount(cmdOff)); + + // Extract the password string + + String pwd = null; + + if (pwdLen > 0) + { + byte[] pwdByt = m_smbPkt.unpackBytes(pwdLen); + pwd = new String(pwdByt); + } + + // Extract the requested share name, as a UNC path + + boolean unicode = m_smbPkt.isUnicode(); + + String uncPath = m_smbPkt.unpackString(unicode); + if (uncPath == null) + { + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return endOff; + } + + // Extract the service type string + + String service = m_smbPkt.unpackString(false); + if (service == null) + { + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return endOff; + } + + // Convert the service type to a shared device type, client may specify '?????' in which + // case we ignore the error. + + int servType = ShareType.ServiceAsType(service); + if (servType == ShareType.UNKNOWN && service.compareTo("?????") != 0) + { + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return endOff; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("NT ANDX Tree Connect AndX - " + uncPath + ", " + service); + + // Parse the requested share name + + PCShare share = null; + + try + { + share = new PCShare(uncPath); + } + catch (InvalidUNCPathException ex) + { + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return endOff; + } + + // Map the IPC$ share to the admin pipe type + + if (servType == ShareType.NAMEDPIPE && share.getShareName().compareTo("IPC$") == 0) + servType = ShareType.ADMINPIPE; + + // Check if the session is a null session, only allow access to the IPC$ named pipe share + + if (m_sess.hasClientInformation() && m_sess.getClientInformation().isNullSession() + && servType != ShareType.ADMINPIPE) + { + + // Return an error status + + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, + SMBStatus.ErrDos); + return endOff; + } + + // Find the requested shared device + + SharedDevice shareDev = null; + + try + { + + // Get/create the shared device + + shareDev = m_sess.getSMBServer().findShare(share.getNodeName(), share.getShareName(), servType, m_sess, + true); + } + catch (InvalidUserException ex) + { + + // Return a logon failure status + + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTLogonFailure, SMBStatus.DOSAccessDenied, + SMBStatus.ErrDos); + return endOff; + } + catch (Exception ex) + { + + // Log the generic error + + logger.error("Exception in TreeConnectAndX", ex); + + // Return a general status, bad network name + + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTBadNetName, SMBStatus.SRVInvalidNetworkName, + SMBStatus.ErrSrv); + return endOff; + } + + // Check if the share is valid + + if (shareDev == null || (servType != ShareType.UNKNOWN && shareDev.getType() != servType)) + { + + // Set the error status + + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTBadNetName, SMBStatus.SRVInvalidNetworkName, + SMBStatus.ErrSrv); + return endOff; + } + + // Authenticate the share connect, if the server is using share mode security + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + int sharePerm = FileAccess.Writeable; + + if (auth != null && auth.getAccessMode() == SrvAuthenticator.SHARE_MODE) + { + + // Validate the share connection + + sharePerm = auth.authenticateShareConnect(m_sess.getClientInformation(), shareDev, pwd, m_sess); + if (sharePerm < 0) + { + + // Invalid share connection request + + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, + SMBStatus.ErrDos); + return endOff; + } + } + + // Check if there is an access control manager, if so then run any access controls to + // determine the + // sessions access to the share. + + if (getSession().getServer().hasAccessControlManager() && shareDev.hasAccessControls()) + { + + // Get the access control manager + + AccessControlManager aclMgr = getSession().getServer().getAccessControlManager(); + + // Update the access permission for this session by processing the access control list + // for the + // shared device + + int aclPerm = aclMgr.checkAccessControl(getSession(), shareDev); + + if (aclPerm == FileAccess.NoAccess) + { + + // Invalid share connection request + + outPkt.setError(m_smbPkt.isLongErrorCode(), SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, + SMBStatus.ErrDos); + return endOff; + } + + // If the access controls returned a new access type update the main permission + + if (aclPerm != AccessControl.Default) + sharePerm = aclPerm; + } + + // Allocate a tree id for the new connection + + TreeConnection tree = null; + + try + { + + // Allocate the tree id for this connection + + int treeId = m_sess.addConnection(shareDev); + outPkt.setTreeId(treeId); + + // Set the file permission that this user has been granted for this share + + tree = m_sess.findConnection(treeId); + tree.setPermission(sharePerm); + + // Inform the driver that a connection has been opened + + if (tree.getInterface() != null) + tree.getInterface().treeOpened(m_sess, tree); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("ANDX Tree Connect AndX - Allocated Tree Id = " + treeId); + } + catch (TooManyConnectionsException ex) + { + + // Too many connections open at the moment + + outPkt.setError(SMBStatus.SRVNoResourcesAvailable, SMBStatus.ErrSrv); + return endOff; + } + + // Build the tree connect response + + outPkt.setAndXParameterCount(endOff, 2); + outPkt.setAndXParameter(endOff, 0, SMBSrvPacket.NO_ANDX_CMD); + outPkt.setAndXParameter(endOff, 1, 0); + + // Pack the service type + + int pos = outPkt.getAndXByteOffset(endOff); + byte[] outBuf = outPkt.getBuffer(); + pos = DataPacker.putString(ShareType.TypeAsService(shareDev.getType()), outBuf, pos, true); + + // Determine the filesystem type, for disk shares + + String devType = ""; + + try + { + // Check if this is a disk shared device + + if ( shareDev.getType() == ShareType.DISK) + { + // Check if the filesystem driver implements the NTFS streams interface, and streams are + // enabled + + if (shareDev.getInterface() instanceof NTFSStreamsInterface) + { + + // Check if NTFS streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) shareDev.getInterface(); + if (ntfsStreams.hasStreamsEnabled(m_sess, tree)) + devType = FileSystem.TypeNTFS; + } + else + { + // Get the filesystem type from the context + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + devType = diskCtx.getFilesystemType(); + } + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Log the error + + logger.error("TreeConnectAndX error", ex); + } + + // Pack the filesystem type + + pos = DataPacker.putString(devType, outBuf, pos, true, outPkt.isUnicode()); + + int bytLen = pos - outPkt.getAndXByteOffset(endOff); + outPkt.setAndXByteCount(endOff, bytLen); + + // Return the new end of packet offset + + return pos; + } + + /** + * Process a chained read file request + * + * @param cmdOff Offset to the chained command within the request packet. + * @param outPkt Reply packet. + * @param endOff Offset to the current end of the reply packet. + * @param netFile File to be read, passed down the chained requests + * @return New end of reply offset. + */ + protected final int procChainedReadAndX(int cmdOff, SMBSrvPacket outPkt, int endOff, NetworkFile netFile) + { + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + outPkt.setError(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return endOff; + } + + // Extract the read file parameters + + long offset = (long) m_smbPkt.getAndXParameterLong(cmdOff, 3); // bottom 32bits of read + // offset + offset &= 0xFFFFFFFFL; + int maxCount = m_smbPkt.getAndXParameter(cmdOff, 5); + + // Check for the NT format request that has the top 32bits of the file offset + + if (m_smbPkt.getAndXParameterCount(cmdOff) == 12) + { + long topOff = (long) m_smbPkt.getAndXParameterLong(cmdOff, 10); + offset += topOff << 32; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Chained File Read AndX : Size=" + maxCount + " ,Pos=" + offset); + + // Read data from the file + + byte[] buf = outPkt.getBuffer(); + int dataPos = 0; + int rdlen = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Set the returned parameter count so that the byte offset can be calculated + + outPkt.setAndXParameterCount(endOff, 12); + dataPos = outPkt.getAndXByteOffset(endOff); + dataPos = DataPacker.wordAlign(dataPos); // align the data buffer + + // Check if the requested data length will fit into the buffer + + int dataLen = buf.length - dataPos; + if (dataLen < maxCount) + maxCount = dataLen; + + // Read from the file + + rdlen = disk.readFile(m_sess, conn, netFile, buf, dataPos, maxCount, offset); + + // Return the data block + + outPkt.setAndXParameter(endOff, 0, SMBSrvPacket.NO_ANDX_CMD); + outPkt.setAndXParameter(endOff, 1, 0); + + outPkt.setAndXParameter(endOff, 2, 0); // bytes remaining, for pipes only + outPkt.setAndXParameter(endOff, 3, 0); // data compaction mode + outPkt.setAndXParameter(endOff, 4, 0); // reserved + outPkt.setAndXParameter(endOff, 5, rdlen); // data length + outPkt.setAndXParameter(endOff, 6, dataPos - RFCNetBIOSProtocol.HEADER_LEN); // offset + // to + // data + + // Clear the reserved parameters + + for (int i = 7; i < 12; i++) + outPkt.setAndXParameter(endOff, i, 0); + + // Set the byte count + + outPkt.setAndXByteCount(endOff, (dataPos + rdlen) - outPkt.getAndXByteOffset(endOff)); + + // Update the end offset for the new end of packet + + endOff = dataPos + rdlen; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + outPkt.setError(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return endOff; + } + catch (java.io.IOException ex) + { + } + + // Return the new end of packet offset + + return endOff; + } + + /** + * Process a chained close file request + * + * @param cmdOff int Offset to the chained command within the request packet. + * @param outPkt SMBSrvPacket Reply packet. + * @param endOff int Offset to the current end of the reply packet. + * @return New end of reply offset. + */ + protected final int procChainedClose(int cmdOff, SMBSrvPacket outPkt, int endOff) + { + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + outPkt.setError(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return endOff; + } + + // Get the file id from the request + + int fid = m_smbPkt.getAndXParameter(cmdOff, 0); + int ftime = m_smbPkt.getAndXParameter(cmdOff, 1); + int fdate = m_smbPkt.getAndXParameter(cmdOff, 2); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + outPkt.setError(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return endOff; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Chained File Close [" + treeId + "] fid=" + fid); + + // Close the file + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Close the file + // + // The disk interface may be null if the file is a named pipe file + + if (disk != null) + disk.closeFile(m_sess, conn, netFile); + + // Indicate that the file has been closed + + netFile.setClosed(true); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + outPkt.setError(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return endOff; + } + catch (java.io.IOException ex) + { + } + + // Clear the returned parameter count and byte count + + outPkt.setAndXParameterCount(endOff, 0); + outPkt.setAndXByteCount(endOff, 0); + + endOff = outPkt.getAndXByteOffset(endOff) - RFCNetBIOSProtocol.HEADER_LEN; + + // Remove the file from the connections list of open files + + conn.removeFile(fid, getSession()); + + // Return the new end of packet offset + + return endOff; + } + + /** + * Process the SMB tree connect request. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + * @exception TooManyConnectionsException Too many concurrent connections on this session. + */ + + protected void procTreeConnectAndX(SMBSrvPacket outPkt) throws SMBSrvException, TooManyConnectionsException, + java.io.IOException + { + + // Check that the received packet looks like a valid tree connect request + + if (m_smbPkt.checkPacketIsValid(4, 3) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Extract the parameters + + int flags = m_smbPkt.getParameter(2); + int pwdLen = m_smbPkt.getParameter(3); + + // Initialize the byte area pointer + + m_smbPkt.resetBytePointer(); + + // Determine if ASCII or unicode strings are being used + + boolean unicode = m_smbPkt.isUnicode(); + + // Extract the password string + + String pwd = null; + + if (pwdLen > 0) + { + byte[] pwdByts = m_smbPkt.unpackBytes(pwdLen); + pwd = new String(pwdByts); + } + + // Extract the requested share name, as a UNC path + + String uncPath = m_smbPkt.unpackString(unicode); + if (uncPath == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Extract the service type string, always seems to be ASCII + + String service = m_smbPkt.unpackString(false); + if (service == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Convert the service type to a shared device type, client may specify '?????' in which + // case we ignore the error. + + int servType = ShareType.ServiceAsType(service); + if (servType == ShareType.UNKNOWN && service.compareTo("?????") != 0) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("NT Tree Connect AndX - " + uncPath + ", " + service); + + // Parse the requested share name + + String shareName = null; + String hostName = null; + + if (uncPath.startsWith("\\")) + { + + try + { + PCShare share = new PCShare(uncPath); + shareName = share.getShareName(); + hostName = share.getNodeName(); + } + catch (InvalidUNCPathException ex) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + } + else + shareName = uncPath; + + // Map the IPC$ share to the admin pipe type + + if (servType == ShareType.NAMEDPIPE && shareName.compareTo("IPC$") == 0) + servType = ShareType.ADMINPIPE; + + // Check if the session is a null session, only allow access to the IPC$ named pipe share + + if (m_sess.hasClientInformation() && m_sess.getClientInformation().isNullSession() + && servType != ShareType.ADMINPIPE) + { + + // Return an error status + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Find the requested shared device + + SharedDevice shareDev = null; + + try + { + + // Get/create the shared device + + shareDev = m_sess.getSMBServer().findShare(hostName, shareName, servType, m_sess, true); + } + catch (InvalidUserException ex) + { + + // Return a logon failure status + + m_sess.sendErrorResponseSMB(SMBStatus.NTLogonFailure, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (Exception ex) + { + + // Log the generic error + + logger.error("TreeConnectAndX error", ex); + + // Return a general status, bad network name + + m_sess.sendErrorResponseSMB(SMBStatus.NTBadNetName, SMBStatus.SRVInvalidNetworkName, SMBStatus.ErrSrv); + return; + } + + // Check if the share is valid + + if (shareDev == null || (servType != ShareType.UNKNOWN && shareDev.getType() != servType)) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTBadNetName, SMBStatus.SRVInvalidNetworkName, SMBStatus.ErrSrv); + return; + } + + // Authenticate the share connection depending upon the security mode the server is running + // under + + SrvAuthenticator auth = getSession().getSMBServer().getAuthenticator(); + int sharePerm = FileAccess.Writeable; + + if (auth != null) + { + + // Validate the share connection + + sharePerm = auth.authenticateShareConnect(m_sess.getClientInformation(), shareDev, pwd, m_sess); + if (sharePerm < 0) + { + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("Tree connect to " + shareName + ", access denied"); + + // Invalid share connection request + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + } + + // Check if there is an access control manager, if so then run any access controls to + // determine the + // sessions access to the share. + + if (getSession().getServer().hasAccessControlManager() && shareDev.hasAccessControls()) + { + + // Get the access control manager + + AccessControlManager aclMgr = getSession().getServer().getAccessControlManager(); + + // Update the access permission for this session by processing the access control list + // for the + // shared device + + int aclPerm = aclMgr.checkAccessControl(getSession(), shareDev); + + if (aclPerm == FileAccess.NoAccess) + { + + // Invalid share connection request + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the access controls returned a new access type update the main permission + + if (aclPerm != AccessControl.Default) + sharePerm = aclPerm; + } + + // Allocate a tree id for the new connection + + int treeId = m_sess.addConnection(shareDev); + outPkt.setTreeId(treeId); + + // Set the file permission that this user has been granted for this share + + TreeConnection tree = m_sess.findConnection(treeId); + tree.setPermission(sharePerm); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TREE)) + logger.debug("Tree Connect AndX - Allocated Tree Id = " + treeId + ", Permission = " + + FileAccess.asString(sharePerm)); + + // Build the tree connect response + + outPkt.setParameterCount(3); + outPkt.setAndXCommand(0xFF); // no chained reply + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 0); + + // Pack the service type + + int pos = outPkt.getByteOffset(); + pos = DataPacker.putString(ShareType.TypeAsService(shareDev.getType()), m_smbPkt.getBuffer(), pos, true); + + // Determine the filesystem type, for disk shares + + String devType = ""; + + try + { + // Check if this is a disk shared device + + if ( shareDev.getType() == ShareType.DISK) + { + // Check if the filesystem driver implements the NTFS streams interface, and streams are + // enabled + + if (shareDev.getInterface() instanceof NTFSStreamsInterface) + { + + // Check if NTFS streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) shareDev.getInterface(); + if (ntfsStreams.hasStreamsEnabled(m_sess, tree)) + devType = FileSystem.TypeNTFS; + } + else + { + // Get the filesystem type from the context + + DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); + devType = diskCtx.getFilesystemType(); + } + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Log the error + + logger.error("TreeConnectAndX error", ex); + } + + // Pack the filesystem type + + pos = DataPacker.putString(devType, m_smbPkt.getBuffer(), pos, true, outPkt.isUnicode()); + outPkt.setByteCount(pos - outPkt.getByteOffset()); + + // Send the response + + m_sess.sendResponseSMB(outPkt); + + // Inform the driver that a connection has been opened + + if (tree.getInterface() != null) + tree.getInterface().treeOpened(m_sess, tree); + } + + /** + * Close a file that has been opened on the server. + * + * @param outPkt Response SMB packet. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procCloseFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file close request + + if (m_smbPkt.checkPacketIsValid(3, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Get the file id from the request + + int fid = m_smbPkt.getParameter(0); + int ftime = m_smbPkt.getParameter(1); + int fdate = m_smbPkt.getParameter(2); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File close [" + treeId + "] fid=" + fid); + + // Close the file + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Close the file + // + // The disk interface may be null if the file is a named pipe file + + if (disk != null) + disk.closeFile(m_sess, conn, netFile); + + // Indicate that the file has been closed + + netFile.setClosed(true); + } + catch (AccessDeniedException ex) + { + // Not allowed to delete the file, when delete on close flag is set + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + } + + // Remove the file from the connections list of open files + + conn.removeFile(fid, getSession()); + + // Build the close file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (netFile.getWriteCount() > 0 && diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileSizeChanged(netFile.getFullName()); + + if (netFile.hasDeleteOnClose() && diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionRemoved, netFile.getFullName()); + } + + /** + * Process a transact2 request. The transact2 can contain many different sub-requests. + * + * @param outPkt SMBSrvPacket + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procTransact2(SMBSrvPacket outPkt) throws IOException, SMBSrvException + { + + // Check that we received enough parameters for a transact2 request + + if (m_smbPkt.checkPacketIsValid(14, 0) == false) + { + + // Not enough parameters for a valid transact2 request + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Create a transact packet using the received SMB packet + + SMBSrvTransPacket tranPkt = new SMBSrvTransPacket(m_smbPkt.getBuffer()); + + // Create a transact buffer to hold the transaction setup, parameter and data blocks + + SrvTransactBuffer transBuf = null; + int subCmd = tranPkt.getSubFunction(); + + if (tranPkt.getTotalParameterCount() == tranPkt.getRxParameterBlockLength() + && tranPkt.getTotalDataCount() == tranPkt.getRxDataBlockLength()) + { + + // Create a transact buffer using the packet buffer, the entire request is contained in + // a single + // packet + + transBuf = new SrvTransactBuffer(tranPkt); + } + else + { + + // Create a transact buffer to hold the multiple transact request parameter/data blocks + + transBuf = new SrvTransactBuffer(tranPkt.getSetupCount(), tranPkt.getTotalParameterCount(), tranPkt + .getTotalDataCount()); + transBuf.setType(tranPkt.getCommand()); + transBuf.setFunction(subCmd); + + // Append the setup, parameter and data blocks to the transaction data + + byte[] buf = tranPkt.getBuffer(); + + transBuf.appendSetup(buf, tranPkt.getSetupOffset(), tranPkt.getSetupCount() * 2); + transBuf.appendParameter(buf, tranPkt.getRxParameterBlock(), tranPkt.getRxParameterBlockLength()); + transBuf.appendData(buf, tranPkt.getRxDataBlock(), tranPkt.getRxDataBlockLength()); + } + + // Set the return data limits for the transaction + + transBuf.setReturnLimits(tranPkt.getMaximumReturnSetupCount(), tranPkt.getMaximumReturnParameterCount(), + tranPkt.getMaximumReturnDataCount()); + + // Check for a multi-packet transaction, for a multi-packet transaction we just acknowledge + // the receive with + // an empty response SMB + + if (transBuf.isMultiPacket()) + { + + // Save the partial transaction data + + m_sess.setTransaction(transBuf); + + // Send an intermediate acknowedgement response + + m_sess.sendSuccessResponseSMB(); + return; + } + + // Check if the transaction is on the IPC$ named pipe, the request requires special + // processing + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + IPCHandler.procTransaction(transBuf, m_sess, outPkt); + return; + } + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction [" + treeId + "] tbuf=" + transBuf); + + // Process the transaction buffer + + processTransactionBuffer(transBuf, outPkt); + } + + /** + * Process a transact2 secondary request. + * + * @param outPkt SMBSrvPacket + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procTransact2Secondary(SMBSrvPacket outPkt) throws IOException, SMBSrvException + { + + // Check that we received enough parameters for a transact2 request + + if (m_smbPkt.checkPacketIsValid(8, 0) == false) + { + + // Not enough parameters for a valid transact2 request + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Check if there is an active transaction, and it is an NT transaction + + if (m_sess.hasTransaction() == false + || (m_sess.getTransaction().isType() == PacketType.Transaction && m_smbPkt.getCommand() != PacketType.TransactionSecond) + || (m_sess.getTransaction().isType() == PacketType.Transaction2 && m_smbPkt.getCommand() != PacketType.Transaction2Second)) + { + + // No transaction to continue, or packet does not match the existing transaction, return + // an error + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Create an NT transaction using the received packet + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(m_smbPkt.getBuffer()); + byte[] buf = tpkt.getBuffer(); + SrvTransactBuffer transBuf = m_sess.getTransaction(); + + // Append the parameter data to the transaction buffer, if any + + int plen = tpkt.getSecondaryParameterBlockCount(); + if (plen > 0) + { + + // Append the data to the parameter buffer + + DataBuffer paramBuf = transBuf.getParameterBuffer(); + paramBuf.appendData(buf, tpkt.getSecondaryParameterBlockOffset(), plen); + } + + // Append the data block to the transaction buffer, if any + + int dlen = tpkt.getSecondaryDataBlockCount(); + if (dlen > 0) + { + + // Append the data to the data buffer + + DataBuffer dataBuf = transBuf.getDataBuffer(); + dataBuf.appendData(buf, tpkt.getSecondaryDataBlockOffset(), dlen); + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction Secondary [" + treeId + "] paramLen=" + plen + ", dataLen=" + dlen); + + // Check if the transaction has been received or there are more sections to be received + + int totParam = tpkt.getTotalParameterCount(); + int totData = tpkt.getTotalDataCount(); + + int paramDisp = tpkt.getParameterBlockDisplacement(); + int dataDisp = tpkt.getDataBlockDisplacement(); + + if ((paramDisp + plen) == totParam && (dataDisp + dlen) == totData) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction complete, processing ..."); + + // Clear the in progress transaction + + m_sess.setTransaction(null); + + // Check if the transaction is on the IPC$ named pipe, the request requires special + // processing + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + IPCHandler.procTransaction(transBuf, m_sess, outPkt); + return; + } + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("Transaction second [" + treeId + "] tbuf=" + transBuf); + + // Process the transaction + + processTransactionBuffer(transBuf, outPkt); + } + else + { + + // There are more transaction parameter/data sections to be received, return an + // intermediate response + + m_sess.sendSuccessResponseSMB(); + } + } + + /** + * Process a transaction buffer + * + * @param tbuf TransactBuffer + * @param outPkt SMBSrvPacket + * @exception IOException If a network error occurs + * @exception SMBSrvException If an SMB error occurs + */ + private final void processTransactionBuffer(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws IOException, + SMBSrvException + { + + // Get the transact2 sub-command code and process the request + + switch (tbuf.getFunction()) + { + + // Start a file search + + case PacketType.Trans2FindFirst: + procTrans2FindFirst(tbuf, outPkt); + break; + + // Continue a file search + + case PacketType.Trans2FindNext: + procTrans2FindNext(tbuf, outPkt); + break; + + // Query file system information + + case PacketType.Trans2QueryFileSys: + procTrans2QueryFileSys(tbuf, outPkt); + break; + + // Query path + + case PacketType.Trans2QueryPath: + procTrans2QueryPath(tbuf, outPkt); + break; + + // Query file information via handle + + case PacketType.Trans2QueryFile: + procTrans2QueryFile(tbuf, outPkt); + break; + + // Set file information via handle + + case PacketType.Trans2SetFile: + procTrans2SetFile(tbuf, outPkt); + break; + + // Set file information via path + + case PacketType.Trans2SetPath: + procTrans2SetPath(tbuf, outPkt); + break; + + // Unknown transact2 command + + default: + + // Return an unrecognized command error + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + break; + } + } + + /** + * Close a search started via the transact2 find first/next command. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procFindClose(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid find close request + + if (m_smbPkt.checkPacketIsValid(1, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Get the search id + + int searchId = m_smbPkt.getParameter(0); + + // Get the search context + + SearchContext ctx = m_sess.getSearchContext(searchId); + + if (ctx == null) + { + + // Invalid search handle + + m_sess.sendSuccessResponseSMB(); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Close trans search [" + searchId + "]"); + + // Deallocate the search slot, close the search. + + m_sess.deallocateSearchSlot(searchId); + + // Return a success status SMB + + m_sess.sendSuccessResponseSMB(); + } + + /** + * Process the file lock/unlock request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procLockingAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid locking andX request + + if (m_smbPkt.checkPacketIsValid(8, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Extract the file lock/unlock parameters + + int fid = m_smbPkt.getParameter(2); + int lockType = m_smbPkt.getParameter(3); + long lockTmo = m_smbPkt.getParameterLong(4); + int unlockCnt = m_smbPkt.getParameter(6); + int lockCnt = m_smbPkt.getParameter(7); + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.Win32InvalidHandle, SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_LOCK)) + logger.debug("File Lock [" + netFile.getFileId() + "] : type=0x" + Integer.toHexString(lockType) + ", tmo=" + + lockTmo + ", locks=" + lockCnt + ", unlocks=" + unlockCnt); + + DiskInterface disk = null; + try + { + + // Get the disk interface for the share + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Check if the virtual filesystem supports file locking + + if (disk instanceof FileLockingInterface) + { + + // Get the lock manager + + FileLockingInterface lockInterface = (FileLockingInterface) disk; + LockManager lockMgr = lockInterface.getLockManager(m_sess, conn); + + // Unpack the lock/unlock structures + + m_smbPkt.resetBytePointer(); + boolean largeFileLock = LockingAndX.hasLargeFiles(lockType); + + // Optimize for a single lock/unlock structure + + if ((unlockCnt + lockCnt) == 1) + { + + // Get the unlock/lock structure + + int pid = m_smbPkt.unpackWord(); + long offset = -1; + long length = -1; + + if (largeFileLock == false) + { + + // Get the lock offset and length, short format + + offset = m_smbPkt.unpackInt(); + length = m_smbPkt.unpackInt(); + } + else + { + + // Get the lock offset and length, large format + + m_smbPkt.skipBytes(2); + + offset = ((long) m_smbPkt.unpackInt()) << 32; + offset += (long) m_smbPkt.unpackInt(); + + length = ((long) m_smbPkt.unpackInt()) << 32; + length += (long) m_smbPkt.unpackInt(); + } + + // Create the lock/unlock details + + FileLock fLock = lockMgr.createLockObject(m_sess, conn, netFile, offset, length, pid); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_LOCK)) + logger.debug(" Single " + (lockCnt == 1 ? "Lock" : "UnLock") + " lock=" + fLock.toString()); + + // Perform the lock/unlock request + + try + { + + // Check if the request is an unlock + + if (unlockCnt > 0) + { + + // Unlock the file + + lockMgr.unlockFile(m_sess, conn, netFile, fLock); + } + else + { + + // Lock the file + + lockMgr.lockFile(m_sess, conn, netFile, fLock); + } + } + catch (NotLockedException ex) + { + + // Return an error status + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNotLocked, SMBStatus.ErrDos); + return; + } + catch (LockConflictException ex) + { + + // Return an error status + + m_sess + .sendErrorResponseSMB(SMBStatus.NTLockNotGranted, SMBStatus.DOSLockConflict, + SMBStatus.ErrDos); + return; + } + catch (IOException ex) + { + + // Return an error status + + m_sess.sendErrorResponseSMB(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + return; + } + } + else + { + + // Unpack the lock/unlock structures + + } + } + else + { + + // Return a 'not locked' status if there are unlocks in the request else return a + // success status + + if (unlockCnt > 0) + { + + // Return an error status + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNotLocked, SMBStatus.ErrDos); + return; + } + } + + // Return a success response + + outPkt.setParameterCount(2); + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); + outPkt.setByteCount(0); + + // Send the lock request response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the logoff request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procLogoffAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid logoff andX request + + if (m_smbPkt.checkPacketIsValid(15, 1) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Return a success status SMB + + m_sess.sendSuccessResponseSMB(); + } + + /** + * Process the file open request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procOpenAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid open andX request + + if (m_smbPkt.checkPacketIsValid(15, 1) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. If the device is + // not a disk type device then return an error. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + else if (conn.getSharedDevice().getType() != ShareType.DISK) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Extract the open file parameters + + int flags = m_smbPkt.getParameter(2); + int access = m_smbPkt.getParameter(3); + int srchAttr = m_smbPkt.getParameter(4); + int fileAttr = m_smbPkt.getParameter(5); + int crTime = m_smbPkt.getParameter(6); + int crDate = m_smbPkt.getParameter(7); + int openFunc = m_smbPkt.getParameter(8); + int allocSiz = m_smbPkt.getParameterLong(9); + + // Extract the filename string + + String fileName = m_smbPkt.unpackString(m_smbPkt.isUnicode()); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Create the file open parameters + + long crDateTime = 0L; + if (crTime > 0 && crDate > 0) + crDateTime = new SMBDate(crDate, crTime).getTime(); + + FileOpenParams params = new FileOpenParams(fileName, openFunc, access, srchAttr, fileAttr, allocSiz, crDateTime); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Open AndX [" + treeId + "] params=" + params); + + // Access the disk interface and open the requested file + + int fid; + NetworkFile netFile = null; + int respAction = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Check if the requested file already exists + + int fileSts = disk.fileExists(m_sess, conn, fileName); + + if (fileSts == FileStatus.NotExist) + { + + // Check if the file should be created if it does not exist + + if (FileAction.createNotExists(openFunc)) + { + + // Create a new file + + netFile = disk.createFile(m_sess, conn, params); + + // Indicate that the file did not exist and was created + + respAction = FileAction.FileCreated; + } + else + { + + // Check if the path is a directory + + if (fileSts == FileStatus.DirectoryExists) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + } + else + { + + // Return a file not found error + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + } + return; + } + } + else + { + + // Open the requested file + + netFile = disk.openFile(m_sess, conn, params); + + // Set the file action response + + if (FileAction.truncateExistingFile(openFunc)) + respAction = FileAction.FileTruncated; + else + respAction = FileAction.FileExisted; + } + + // Add the file to the list of open files for this tree connection + + fid = conn.addFile(netFile, getSession()); + + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + m_sess.sendErrorResponseSMB(SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (FileSharingException ex) + { + + // Return a sharing violation error + + m_sess.sendErrorResponseSMB(SMBStatus.NTSharingViolation, SMBStatus.DOSFileSharingConflict, + SMBStatus.ErrDos); + return; + } + catch (FileOfflineException ex) + { + + // File data is unavailable + + m_sess.sendErrorResponseSMB(SMBStatus.NTFileOffline, SMBStatus.HRDDriveNotReady, SMBStatus.ErrHrd); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the open file response + + outPkt.setParameterCount(15); + + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); // AndX offset + + outPkt.setParameter(2, fid); + outPkt.setParameter(3, netFile.getFileAttributes()); // file attributes + + SMBDate modDate = null; + + if (netFile.hasModifyDate()) + modDate = new SMBDate(netFile.getModifyDate()); + + outPkt.setParameter(4, modDate != null ? modDate.asSMBTime() : 0); // last write time + outPkt.setParameter(5, modDate != null ? modDate.asSMBDate() : 0); // last write date + outPkt.setParameterLong(6, netFile.getFileSizeInt()); // file size + outPkt.setParameter(8, netFile.getGrantedAccess()); + outPkt.setParameter(9, OpenAndX.FileTypeDisk); + outPkt.setParameter(10, 0); // named pipe state + outPkt.setParameter(11, respAction); + outPkt.setParameter(12, 0); // server FID (long) + outPkt.setParameter(13, 0); + outPkt.setParameter(14, 0); + + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process the file read request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procReadAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid read andX request + + if (m_smbPkt.checkPacketIsValid(10, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + + // Extract the read file parameters + + int fid = m_smbPkt.getParameter(2); + long offset = (long) m_smbPkt.getParameterLong(3); // bottom 32bits of read offset + offset &= 0xFFFFFFFFL; + int maxCount = m_smbPkt.getParameter(5); + + // Check for the NT format request that has the top 32bits of the file offset + + if (m_smbPkt.getParameterCount() == 12) + { + long topOff = (long) m_smbPkt.getParameterLong(10); + offset += topOff << 32; + } + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Read AndX [" + netFile.getFileId() + "] : Size=" + maxCount + " ,Pos=" + offset); + + // Read data from the file + + byte[] buf = outPkt.getBuffer(); + int dataPos = 0; + int rdlen = 0; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Set the returned parameter count so that the byte offset can be calculated + + outPkt.setParameterCount(12); + dataPos = outPkt.getByteOffset(); + dataPos = DataPacker.wordAlign(dataPos); // align the data buffer + + // Check if the requested data length will fit into the buffer + + int dataLen = buf.length - dataPos; + if (dataLen < maxCount) + maxCount = dataLen; + + // Read from the file + + rdlen = disk.readFile(m_sess, conn, netFile, buf, dataPos, maxCount, offset); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (FileOfflineException ex) + { + + // File data is unavailable + + m_sess.sendErrorResponseSMB(SMBStatus.NTFileOffline, SMBStatus.HRDReadFault, SMBStatus.ErrHrd); + return; + } + catch (LockConflictException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_LOCK)) + logger.debug("Read Lock Error [" + netFile.getFileId() + "] : Size=" + maxCount + " ,Pos=" + offset); + + // File is locked + + m_sess.sendErrorResponseSMB(SMBStatus.NTLockConflict, SMBStatus.DOSLockConflict, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // User does not have the required access rights or file is not accessible + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDReadFault, SMBStatus.ErrHrd); + return; + } + + // Return the data block + + outPkt.setAndXCommand(0xFF); // no chained command + outPkt.setParameter(1, 0); + outPkt.setParameter(2, 0); // bytes remaining, for pipes only + outPkt.setParameter(3, 0); // data compaction mode + outPkt.setParameter(4, 0); // reserved + outPkt.setParameter(5, rdlen); // data length + outPkt.setParameter(6, dataPos - RFCNetBIOSProtocol.HEADER_LEN); // offset to data + + // Clear the reserved parameters + + for (int i = 7; i < 12; i++) + outPkt.setParameter(i, 0); + + // Set the byte count + + outPkt.setByteCount((dataPos + rdlen) - outPkt.getByteOffset()); + + // Check if there is a chained command, or commands + + if (m_smbPkt.hasAndXCommand()) + { + + // Process any chained commands, AndX + + int pos = procAndXCommands(outPkt, outPkt.getPacketLength(), netFile); + + // Send the read andX response + + m_sess.sendResponseSMB(outPkt, pos); + } + else + { + + // Send the normal read andX response + + m_sess.sendResponseSMB(outPkt); + } + } + + /** + * Rename a file. + * + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected void procRenameFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid rename file request + + if (m_smbPkt.checkPacketIsValid(1, 4) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the Unicode flag + + boolean isUni = m_smbPkt.isUnicode(); + + // Read the data block + + m_smbPkt.resetBytePointer(); + + // Extract the old file name + + if (m_smbPkt.unpackByte() != DataType.ASCII) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + String oldName = m_smbPkt.unpackString(isUni); + if (oldName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Extract the new file name + + if (m_smbPkt.unpackByte() != DataType.ASCII) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + String newName = m_smbPkt.unpackString(isUni); + if (newName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Rename [" + treeId + "] old name=" + oldName + ", new name=" + newName); + + // Access the disk interface and rename the requested file + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Rename the requested file + + disk.renameFile(m_sess, conn, oldName, newName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (FileNotFoundException ex) + { + + // Source file/directory does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (FileExistsException ex) + { + + // Destination file/directory already exists + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to rename the file/directory + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (FileSharingException ex) + { + + // Return a sharing violation error + + m_sess.sendErrorResponseSMB(SMBStatus.NTSharingViolation, SMBStatus.DOSFileSharingConflict, + SMBStatus.ErrDos); + return; + } + + // Build the rename file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyRename(oldName, newName); + } + + /** + * Delete a file. + * + * @param outPkt SMBSrvPacket + * @exception IOException If an network error occurs + * @exception SMBSrvException If an SMB error occurs + */ + protected void procDeleteFile(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid file delete request + + if (m_smbPkt.checkPacketIsValid(1, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the Unicode flag + + boolean isUni = m_smbPkt.isUnicode(); + + // Read the data block + + m_smbPkt.resetBytePointer(); + + // Extract the old file name + + if (m_smbPkt.unpackByte() != DataType.ASCII) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + String fileName = m_smbPkt.unpackString(isUni); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("File Delete [" + treeId + "] name=" + fileName); + + // Access the disk interface and delete the file(s) + + int fid; + NetworkFile netFile = null; + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Delete file(s) + + disk.deleteFile(m_sess, conn, fileName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to delete the file + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the delete file response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionRemoved, fileName); + } + + /** + * Delete a directory. + * + * @param outPkt SMBSrvPacket + * @exception IOException If a network error occurs + * @exception SMBSrvException If an SMB error occurs + */ + protected void procDeleteDirectory(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid delete directory request + + if (m_smbPkt.checkPacketIsValid(0, 2) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the Unicode flag + + boolean isUni = m_smbPkt.isUnicode(); + + // Read the data block + + m_smbPkt.resetBytePointer(); + + // Extract the old file name + + if (m_smbPkt.unpackByte() != DataType.ASCII) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + String dirName = m_smbPkt.unpackString(isUni); + if (dirName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("Directory Delete [" + treeId + "] name=" + dirName); + + // Access the disk interface and delete the directory + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Delete the directory + + disk.deleteDirectory(m_sess, conn, dirName); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to delete the directory + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (DirectoryNotEmptyException ex) + { + + // Directory not empty + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryNotEmpty, SMBStatus.ErrDos); + return; + } + catch (java.io.IOException ex) + { + + // Failed to delete the directory + + m_sess.sendErrorResponseSMB(SMBStatus.DOSDirectoryInvalid, SMBStatus.ErrDos); + return; + } + + // Build the delete directory response + + outPkt.setParameterCount(0); + outPkt.setByteCount(0); + + // Send the response packet + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (diskCtx.hasChangeHandler()) + diskCtx.getChangeHandler().notifyDirectoryChanged(NotifyChange.ActionRemoved, dirName); + } + + /** + * Process a transact2 file search request. + * + * @param tbuf Transaction request details + * @param outPkt Packet to use for the reply. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2FindFirst(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv); + return; + } + + // Get the search parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int srchAttr = paramBuf.getShort(); + int maxFiles = paramBuf.getShort(); + int srchFlag = paramBuf.getShort(); + int infoLevl = paramBuf.getShort(); + paramBuf.skipBytes(4); + + String srchPath = paramBuf.getString(tbuf.isUnicode()); + + // Check if the search path is valid + + if (srchPath == null || srchPath.length() == 0) + { + + // Invalid search request + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + else if (srchPath.endsWith("\\")) + { + + // Make the search a wildcard search + + srchPath = srchPath + "*.*"; + } + + // Check for the Macintosh information level, if the Macintosh extensions are not enabled + // return an error + + if (infoLevl == FindInfoPacker.InfoMacHfsInfo && getSession().hasMacintoshExtensions() == false) + { + + // Return an error status, Macintosh extensions are not enabled + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Access the shared device disk interface + + SearchContext ctx = null; + DiskInterface disk = null; + int searchId = -1; + boolean wildcardSearch = false; + + try + { + + // Access the disk interface + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Allocate a search slot for the new search + + searchId = m_sess.allocateSearchSlot(); + if (searchId == -1) + { + + // Failed to allocate a slot for the new search + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNoResourcesAvailable, SMBStatus.ErrSrv); + return; + } + + // Check if this is a wildcard search or single file search + + if (WildCard.containsWildcards(srchPath) || WildCard.containsUnicodeWildcard(srchPath)) + wildcardSearch = true; + + // Check if the search contains Unicode wildcards + + if (tbuf.isUnicode() && WildCard.containsUnicodeWildcard(srchPath)) + { + + // Translate the Unicode wildcards to standard DOS wildcards + + srchPath = WildCard.convertUnicodeWildcardToDOS(srchPath); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Converted Unicode wildcards to:" + srchPath); + } + + // Start a new search + + ctx = disk.startSearch(m_sess, conn, srchPath, srchAttr); + if (ctx != null) + { + + // Store details of the search in the context + + ctx.setTreeId(treeId); + ctx.setMaximumFiles(maxFiles); + } + else + { + + // Failed to start the search, return a no more files error + + m_sess.sendErrorResponseSMB(SMBStatus.NTNoSuchFile, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Save the search context + + m_sess.setSearchContext(searchId, ctx); + + // Create the reply transact buffer + + SrvTransactBuffer replyBuf = new SrvTransactBuffer(tbuf); + DataBuffer dataBuf = replyBuf.getDataBuffer(); + + // Determine the maximum return data length + + int maxLen = replyBuf.getReturnDataLimit(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Start trans search [" + searchId + "] - " + srchPath + ", attr=0x" + + Integer.toHexString(srchAttr) + ", maxFiles=" + maxFiles + ", maxLen=" + maxLen + + ", infoLevel=" + infoLevl + ", flags=0x" + Integer.toHexString(srchFlag)); + + // Loop until we have filled the return buffer or there are no more files to return + + int fileCnt = 0; + int packLen = 0; + int lastNameOff = 0; + + // Flag to indicate if resume ids should be returned + + boolean resumeIds = false; + if (infoLevl == FindInfoPacker.InfoStandard && (srchFlag & FindFirstNext.ReturnResumeKey) != 0) + { + + // Windows servers only seem to return resume keys for the standard information + // level + + resumeIds = true; + } + + // If this is a wildcard search then add the '.' and '..' entries + + if (wildcardSearch == true && ReturnDotFiles == true) + { + + // Pack the '.' file information + + if (resumeIds == true) + { + dataBuf.putInt(-1); + maxLen -= 4; + } + + lastNameOff = dataBuf.getPosition(); + FileInfo dotInfo = new FileInfo(".", 0, FileAttribute.Directory); + dotInfo.setFileId(dotInfo.getFileName().hashCode()); + + packLen = FindInfoPacker.packInfo(dotInfo, dataBuf, infoLevl, tbuf.isUnicode()); + + // Update the file count for this packet, update the remaining buffer length + + fileCnt++; + maxLen -= packLen; + + // Pack the '..' file information + + if (resumeIds == true) + { + dataBuf.putInt(-2); + maxLen -= 4; + } + + lastNameOff = dataBuf.getPosition(); + dotInfo.setFileName(".."); + dotInfo.setFileId(dotInfo.getFileName().hashCode()); + + packLen = FindInfoPacker.packInfo(dotInfo, dataBuf, infoLevl, tbuf.isUnicode()); + + // Update the file count for this packet, update the remaining buffer length + + fileCnt++; + maxLen -= packLen; + } + + boolean pktDone = false; + boolean searchDone = false; + + FileInfo info = new FileInfo(); + + while (pktDone == false && fileCnt < maxFiles) + { + + // Get file information from the search + + if (ctx.nextFileInfo(info) == false) + { + + // No more files + + pktDone = true; + searchDone = true; + } + + // Check if the file information will fit into the return buffer + + else if (FindInfoPacker.calcInfoSize(info, infoLevl, false, true) <= maxLen) + { + + // Pack the resume id, if required + + if (resumeIds == true) + { + dataBuf.putInt(ctx.getResumeId()); + maxLen -= 4; + } + + // Save the offset to the last file information structure + + lastNameOff = dataBuf.getPosition(); + + // Pack the file information + + packLen = FindInfoPacker.packInfo(info, dataBuf, infoLevl, tbuf.isUnicode()); + + // Update the file count for this packet + + fileCnt++; + + // Recalculate the remaining buffer space + + maxLen -= packLen; + } + else + { + + // Set the search restart point + + ctx.restartAt(info); + + // No more buffer space + + pktDone = true; + } + } + + // Check for a single file search and the file was not found, in this case return an + // error status + + if (wildcardSearch == false && fileCnt == 0) + throw new FileNotFoundException(srchPath); + + // Check for a search where the maximum files is set to one, close the search + // immediately. + + if (maxFiles == 1 && fileCnt == 1) + searchDone = true; + + // Clear the next structure offset, if applicable + + FindInfoPacker.clearNextOffset(dataBuf, infoLevl, lastNameOff); + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(searchId); + paramBuf.putShort(fileCnt); + paramBuf.putShort(ctx.hasMoreFiles() ? 0 : 1); + paramBuf.putShort(0); + paramBuf.putShort(lastNameOff); + + // Send the transaction response + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(outPkt.getBuffer()); + tpkt.doTransactionResponse(m_sess, replyBuf); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search [" + searchId + "] Returned " + fileCnt + " files, dataLen=" + dataBuf.getLength() + + ", moreFiles=" + ctx.hasMoreFiles()); + + // Check if the search is complete + + if (searchDone == true || ctx.hasMoreFiles() == false) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("End start search [" + searchId + "] (Search complete)"); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + } + } + catch (FileNotFoundException ex) + { + + // Search path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTNoSuchFile, SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + } + catch (PathNotFoundException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Requested path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectPathNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + } + catch (UnsupportedInfoLevelException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + } + + /** + * Process a transact2 file search continue request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2FindNext(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the search parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int searchId = paramBuf.getShort(); + int maxFiles = paramBuf.getShort(); + int infoLevl = paramBuf.getShort(); + int reskey = paramBuf.getInt(); + int srchFlag = paramBuf.getShort(); + + String resumeName = paramBuf.getString(tbuf.isUnicode()); + + // Access the shared device disk interface + + SearchContext ctx = null; + DiskInterface disk = null; + + try + { + + // Access the disk interface + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Retrieve the search context + + ctx = m_sess.getSearchContext(searchId); + if (ctx == null) + { + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search context null - [" + searchId + "]"); + + // Invalid search handle + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + return; + } + + // Create the reply transaction buffer + + SrvTransactBuffer replyBuf = new SrvTransactBuffer(tbuf); + DataBuffer dataBuf = replyBuf.getDataBuffer(); + + // Determine the maximum return data length + + int maxLen = replyBuf.getReturnDataLimit(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Continue search [" + searchId + "] - " + resumeName + ", maxFiles=" + maxFiles + + ", maxLen=" + maxLen + ", infoLevel=" + infoLevl + ", flags=0x" + + Integer.toHexString(srchFlag)); + + // Loop until we have filled the return buffer or there are no more files to return + + int fileCnt = 0; + int packLen = 0; + int lastNameOff = 0; + + // Flag to indicate if resume ids should be returned + + boolean resumeIds = false; + if (infoLevl == FindInfoPacker.InfoStandard && (srchFlag & FindFirstNext.ReturnResumeKey) != 0) + { + + // Windows servers only seem to return resume keys for the standard information + // level + + resumeIds = true; + } + + // Flags to indicate packet full or search complete + + boolean pktDone = false; + boolean searchDone = false; + + FileInfo info = new FileInfo(); + + while (pktDone == false && fileCnt < maxFiles) + { + + // Get file information from the search + + if (ctx.nextFileInfo(info) == false) + { + + // No more files + + pktDone = true; + searchDone = true; + } + + // Check if the file information will fit into the return buffer + + else if (FindInfoPacker.calcInfoSize(info, infoLevl, false, true) <= maxLen) + { + + // Pack the resume id, if required + + if (resumeIds == true) + { + dataBuf.putInt(ctx.getResumeId()); + maxLen -= 4; + } + + // Save the offset to the last file information structure + + lastNameOff = dataBuf.getPosition(); + + // Pack the file information + + packLen = FindInfoPacker.packInfo(info, dataBuf, infoLevl, tbuf.isUnicode()); + + // Update the file count for this packet + + fileCnt++; + + // Recalculate the remaining buffer space + + maxLen -= packLen; + } + else + { + + // Set the search restart point + + ctx.restartAt(info); + + // No more buffer space + + pktDone = true; + } + } + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(fileCnt); + paramBuf.putShort(ctx.hasMoreFiles() ? 0 : 1); + paramBuf.putShort(0); + paramBuf.putShort(lastNameOff); + + // Send the transaction response + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(outPkt.getBuffer()); + tpkt.doTransactionResponse(m_sess, replyBuf); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("Search [" + searchId + "] Returned " + fileCnt + " files, dataLen=" + dataBuf.getLength() + + ", moreFiles=" + ctx.hasMoreFiles()); + + // Check if the search is complete + + if (searchDone == true || ctx.hasMoreFiles() == false) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_SEARCH)) + logger.debug("End start search [" + searchId + "] (Search complete)"); + + // Release the search context + + m_sess.deallocateSearchSlot(searchId); + } + } + catch (FileNotFoundException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Search path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.DOSNoMoreFiles, SMBStatus.ErrDos); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + } + catch (UnsupportedInfoLevelException ex) + { + + // Deallocate the search + + if (searchId != -1) + m_sess.deallocateSearchSlot(searchId); + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + } + + /** + * Process a transact2 file system query request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2QueryFileSys(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) + throws java.io.IOException, SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the query file system required information level + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevl = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Query File System Info - level = 0x" + Integer.toHexString(infoLevl)); + + // Access the shared device disk interface + + try + { + + // Access the disk interface and context + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the disk information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = DataPacker.longwordAlign(outPkt.getByteOffset()); + int dataPos = prmPos; // no parameters returned + + // Create a data buffer using the SMB packet. The response should always fit into a + // single + // reply packet. + + DataBuffer replyBuf = new DataBuffer(buf, dataPos, buf.length - dataPos); + + // Determine the information level requested + + SrvDiskInfo diskInfo = null; + VolumeInfo volInfo = null; + + switch (infoLevl) + { + + // Standard disk information + + case DiskInfoPacker.InfoStandard: + + // Get the disk information + + diskInfo = getDiskInformation(disk, diskCtx); + + // Pack the disk information into the return data packet + + DiskInfoPacker.packStandardInfo(diskInfo, replyBuf); + break; + + // Volume label information + + case DiskInfoPacker.InfoVolume: + + // Get the volume label information + + volInfo = getVolumeInformation(disk, diskCtx); + + // Pack the volume label information + + DiskInfoPacker.packVolumeInfo(volInfo, replyBuf, tbuf.isUnicode()); + break; + + // Full volume information + + case DiskInfoPacker.InfoFsVolume: + + // Get the volume information + + volInfo = getVolumeInformation(disk, diskCtx); + + // Pack the volume information + + DiskInfoPacker.packFsVolumeInformation(volInfo, replyBuf, tbuf.isUnicode()); + break; + + // Filesystem size information + + case DiskInfoPacker.InfoFsSize: + + // Get the disk information + + diskInfo = getDiskInformation(disk, diskCtx); + + // Pack the disk information into the return data packet + + DiskInfoPacker.packFsSizeInformation(diskInfo, replyBuf); + break; + + // Filesystem device information + + case DiskInfoPacker.InfoFsDevice: + DiskInfoPacker.packFsDevice(NTIOCtl.DeviceDisk, diskCtx.getDeviceAttributes(), replyBuf); + break; + + // Filesystem attribute information + + case DiskInfoPacker.InfoFsAttribute: + String fsType = diskCtx.getFilesystemType(); + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if NTFS streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + if (ntfsStreams.hasStreamsEnabled(m_sess, conn)) + fsType = "NTFS"; + } + + // Pack the filesystem type + + DiskInfoPacker.packFsAttribute(diskCtx.getFilesystemAttributes(), 255, fsType, tbuf.isUnicode(), + replyBuf); + break; + + // Mac filesystem information + + case DiskInfoPacker.InfoMacFsInfo: + + // Check if the filesystem supports NTFS streams + // + // We should only return a valid response to the Macintosh information level if the + // filesystem + // does NOT support NTFS streams. By returning an error status the Thursby DAVE + // software will treat + // the filesystem as a WinXP/2K filesystem with full streams support. + + boolean ntfs = false; + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + ntfs = ntfsStreams.hasStreamsEnabled(m_sess, conn); + } + + // If the filesystem does not support NTFS streams then send a valid response. + + if (ntfs == false) + { + + // Get the disk and volume information + + diskInfo = getDiskInformation(disk, diskCtx); + volInfo = getVolumeInformation(disk, diskCtx); + + // Pack the disk information into the return data packet + + DiskInfoPacker.packMacFsInformation(diskInfo, volInfo, ntfs, replyBuf); + } + break; + + // Filesystem size information, including per user allocation limit + + case DiskInfoPacker.InfoFullFsSize: + + // Get the disk information + + diskInfo = getDiskInformation(disk, diskCtx); + long userLimit = diskInfo.getTotalUnits(); + + // Pack the disk information into the return data packet + + DiskInfoPacker.packFullFsSizeInformation(userLimit, diskInfo, replyBuf); + break; + } + + // Check if any data was packed, if not then the information level is not supported + + if (replyBuf.getPosition() == dataPos) + { + m_sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + int bytCnt = replyBuf.getPosition() - outPkt.getByteOffset(); + replyBuf.setEndOfBuffer(); + int dataLen = replyBuf.getLength(); + SMBSrvTransPacket.initTransactReply(outPkt, 0, prmPos, dataLen, dataPos); + outPkt.setByteCount(bytCnt); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + } + + /** + * Process a transact2 query path information request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + protected final void procTrans2QueryPath(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the query path information level and file/directory name + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevl = paramBuf.getShort(); + paramBuf.skipBytes(4); + + String path = paramBuf.getString(tbuf.isUnicode()); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Query Path - level = 0x" + Integer.toHexString(infoLevl) + ", path = " + path); + + // Access the shared device disk interface + + try + { + + // Access the disk interface + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the file information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = DataPacker.longwordAlign(outPkt.getByteOffset()); + int dataPos = prmPos + 4; + + // Pack the return parametes, EA error offset + + outPkt.setPosition(prmPos); + outPkt.packWord(0); + + // Create a data buffer using the SMB packet. The response should always fit into a + // single + // reply packet. + + DataBuffer replyBuf = new DataBuffer(buf, dataPos, buf.length - dataPos); + + // Check if the virtual filesystem supports streams, and streams are enabled + + boolean streams = false; + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if NTFS streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + streams = ntfsStreams.hasStreamsEnabled(m_sess, conn); + } + + // Check if the path is for an NTFS stream, return an error if streams are not supported or not enabled + + if ( streams == false && path.indexOf(FileOpenParams.StreamSeparator) != -1) + { + // NTFS streams not supported, return an error status + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameInvalid, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Check for the file streams information level + + int dataLen = 0; + + if (streams == true + && (infoLevl == FileInfoLevel.PathFileStreamInfo || infoLevl == FileInfoLevel.NTFileStreamInfo)) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_STREAMS)) + logger.debug("Get NTFS streams list path=" + path); + + // Get the list of streams from the share driver + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + StreamInfoList streamList = ntfsStreams.getStreamList(m_sess, conn, path); + + if (streamList == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + + // Pack the file streams information into the return data packet + + dataLen = QueryInfoPacker.packStreamFileInfo(streamList, replyBuf, true); + } + else + { + + // Get the file information + + FileInfo fileInfo = disk.getFileInformation(m_sess, conn, path); + + if (fileInfo == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, + SMBStatus.ErrDos); + return; + } + + // Pack the file information into the return data packet + + dataLen = QueryInfoPacker.packInfo(fileInfo, replyBuf, infoLevl, true); + } + + // Check if any data was packed, if not then the information level is not supported + + if (dataLen == 0) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + + SMBSrvTransPacket.initTransactReply(outPkt, 2, prmPos, dataLen, dataPos); + outPkt.setByteCount(replyBuf.getPosition() - outPkt.getByteOffset()); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + } + catch (FileNotFoundException ex) + { + + // Requested file does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (PathNotFoundException ex) + { + + // Requested path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectPathNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + catch (UnsupportedInfoLevelException ex) + { + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + } + + /** + * Process a transact2 query file information (via handle) request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException SMB protocol exception + */ + protected final void procTrans2QueryFile(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id and query path information level + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int fid = paramBuf.getShort(); + int infoLevl = paramBuf.getShort(); + + // Get the file details via the file id + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Query File - level=0x" + Integer.toHexString(infoLevl) + ", fid=" + fid + ", stream=" + + netFile.getStreamId() + ", name=" + netFile.getFullName()); + + // Access the shared device disk interface + + try + { + + // Access the disk interface + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the file information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = DataPacker.longwordAlign(outPkt.getByteOffset()); + int dataPos = prmPos + 4; + + // Pack the return parametes, EA error offset + + outPkt.setPosition(prmPos); + outPkt.packWord(0); + + // Create a data buffer using the SMB packet. The response should always fit into a + // single + // reply packet. + + DataBuffer replyBuf = new DataBuffer(buf, dataPos, buf.length - dataPos); + + // Check if the virtual filesystem supports streams, and streams are enabled + + boolean streams = false; + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if NTFS streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + streams = ntfsStreams.hasStreamsEnabled(m_sess, conn); + } + + // Check for the file streams information level + + int dataLen = 0; + + if (streams == true + && (infoLevl == FileInfoLevel.PathFileStreamInfo || infoLevl == FileInfoLevel.NTFileStreamInfo)) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_STREAMS)) + logger.debug("Get NTFS streams list fid=" + fid + ", name=" + netFile.getFullName()); + + // Get the list of streams from the share driver + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + StreamInfoList streamList = ntfsStreams.getStreamList(m_sess, conn, netFile.getFullName()); + + if (streamList == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + + // Pack the file streams information into the return data packet + + dataLen = QueryInfoPacker.packStreamFileInfo(streamList, replyBuf, true); + } + else + { + + // Get the file information + + FileInfo fileInfo = disk.getFileInformation(m_sess, conn, netFile.getFullNameStream()); + + if (fileInfo == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + + // Pack the file information into the return data packet + + dataLen = QueryInfoPacker.packInfo(fileInfo, replyBuf, infoLevl, true); + } + + // Check if any data was packed, if not then the information level is not supported + + if (dataLen == 0) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, + SMBStatus.ErrSrv); + return; + } + + SMBSrvTransPacket.initTransactReply(outPkt, 2, prmPos, dataLen, dataPos); + outPkt.setByteCount(replyBuf.getPosition() - outPkt.getByteOffset()); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + } + catch (FileNotFoundException ex) + { + + // Requested file does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (PathNotFoundException ex) + { + + // Requested path does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectPathNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + catch (UnsupportedInfoLevelException ex) + { + + // Requested information level is not supported + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + } + + /** + * Process a transact2 set file information (via handle) request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException SMB protocol exception + */ + protected final void procTrans2SetFile(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file id and information level + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int fid = paramBuf.getShort(); + int infoLevl = paramBuf.getShort(); + + // Get the file details via the file id + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Set File - level=0x" + Integer.toHexString(infoLevl) + ", fid=" + fid + ", name=" + + netFile.getFullName()); + + // Access the shared device disk interface + + try + { + + // Access the disk interface + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Process the set file information request + + DataBuffer dataBuf = tbuf.getDataBuffer(); + FileInfo finfo = null; + + switch (infoLevl) + { + + // Set basic file information (dates/attributes) + + case FileInfoLevel.SetBasicInfo: + + // Create the file information template + + int setFlags = 0; + finfo = new FileInfo(netFile.getFullName(), 0, -1); + + // Set the creation date/time, if specified + + long timeNow = System.currentTimeMillis(); + + long nttim = dataBuf.getLong(); + boolean hasSetTime = false; + + if (nttim != 0L) + { + if (nttim != -1L) + { + finfo.setCreationDateTime(NTTime.toJavaDate(nttim)); + setFlags += FileInfo.SetCreationDate; + } + hasSetTime = true; + } + + // Set the last access date/time, if specified + + nttim = dataBuf.getLong(); + + if (nttim != 0L) + { + if (nttim != -1L) + { + finfo.setAccessDateTime(NTTime.toJavaDate(nttim)); + setFlags += FileInfo.SetAccessDate; + } + else + { + finfo.setAccessDateTime(timeNow); + setFlags += FileInfo.SetAccessDate; + } + hasSetTime = true; + } + + // Set the last write date/time, if specified + + nttim = dataBuf.getLong(); + + if (nttim > 0L) + { + if (nttim != -1L) + { + finfo.setModifyDateTime(NTTime.toJavaDate(nttim)); + setFlags += FileInfo.SetModifyDate; + } + else + { + finfo.setModifyDateTime(timeNow); + setFlags += FileInfo.SetModifyDate; + } + hasSetTime = true; + } + + // Set the modify date/time, if specified + + nttim = dataBuf.getLong(); + + if (nttim > 0L) + { + if (nttim != -1L) + { + finfo.setChangeDateTime(NTTime.toJavaDate(nttim)); + setFlags += FileInfo.SetChangeDate; + } + hasSetTime = true; + } + + // Set the attributes + + int attr = dataBuf.getInt(); + int unknown = dataBuf.getInt(); + + if (hasSetTime == false && unknown == 0) + { + finfo.setFileAttributes(attr); + setFlags += FileInfo.SetAttributes; + } + + // Set the file information for the specified file/directory + + finfo.setFileInformationFlags(setFlags); + disk.setFileInformation(m_sess, conn, netFile.getFullName(), finfo); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Set Basic Info [" + treeId + "] name=" + netFile.getFullName() + ", attr=0x" + + Integer.toHexString(attr) + ", setTime=" + hasSetTime + ", setFlags=0x" + + Integer.toHexString(setFlags) + ", unknown=" + unknown); + break; + + // Set end of file position for a file + + case FileInfoLevel.SetEndOfFileInfo: + + // Get the new end of file position + + long eofPos = dataBuf.getLong(); + + // Set the new end of file position + + disk.truncateFile(m_sess, conn, netFile, eofPos); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Set end of file position fid=" + fid + ", eof=" + eofPos); + break; + + // Set the allocation size for a file + + case FileInfoLevel.SetAllocationInfo: + + // Get the new end of file position + + long allocSize = dataBuf.getLong(); + + // Set the new end of file position + + disk.truncateFile(m_sess, conn, netFile, allocSize); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Set allocation size fid=" + fid + ", allocSize=" + allocSize); + break; + + // Rename a stream + + case FileInfoLevel.NTFileRenameInfo: + + // Check if the virtual filesystem supports streams, and streams are enabled + + boolean streams = false; + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if NTFS streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + streams = ntfsStreams.hasStreamsEnabled(m_sess, conn); + } + + // If streams are not supported or are not enabled then return an error status + + if (streams == false) + { + + // Return a not supported error status + + m_sess.sendErrorResponseSMB(SMBStatus.NTNotSupported, SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Get the overwrite flag + + boolean overwrite = dataBuf.getByte() == 1 ? true : false; + dataBuf.skipBytes(3); + + int rootFid = dataBuf.getInt(); + int nameLen = dataBuf.getInt(); + String newName = dataBuf.getString(nameLen, true); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Set rename fid=" + fid + ", newName=" + newName + ", overwrite=" + overwrite + + ", rootFID=" + rootFid); + + // Check if the new path contains a directory, only rename of a stream on the same + // file is supported + + if (newName.indexOf(FileName.DOS_SEPERATOR_STR) != -1) + { + + // Return a not supported error status + + m_sess.sendErrorResponseSMB(SMBStatus.NTNotSupported, SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_STREAMS)) + logger.debug("Rename stream fid=" + fid + ", name=" + netFile.getFullNameStream() + ", newName=" + + newName + ", overwrite=" + overwrite); + + // Rename the stream + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + ntfsStreams.renameStream(m_sess, conn, netFile.getFullNameStream(), newName, overwrite); + break; + + // Mark or unmark a file/directory for delete + + case FileInfoLevel.SetDispositionInfo: + case FileInfoLevel.NTFileDispositionInfo: + + // Get the delete flag + + int flag = dataBuf.getByte(); + boolean delFlag = flag == 1 ? true : false; + + // Call the filesystem driver set file information to see if the file can be marked + // for + // delete. + + FileInfo delInfo = new FileInfo(); + delInfo.setDeleteOnClose(delFlag); + delInfo.setFileInformationFlags(FileInfo.SetDeleteOnClose); + + disk.setFileInformation(m_sess, conn, netFile.getFullName(), delInfo); + + // Mark/unmark the file/directory for deletion + + netFile.setDeleteOnClose(delFlag); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Set file disposition fid=" + fid + ", name=" + netFile.getName() + ", delete=" + + delFlag); + break; + } + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the return information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = outPkt.getByteOffset(); + + // Longword align the parameters, return an unknown word parameter + // + // Note: Make sure the data offset is on a longword boundary, NT has problems if this is + // not done + + prmPos = DataPacker.longwordAlign(prmPos); + DataPacker.putIntelShort(0, buf, prmPos); + + SMBSrvTransPacket.initTransactReply(outPkt, 2, prmPos, 0, prmPos + 4); + outPkt.setByteCount((prmPos - outPkt.getByteOffset()) + 4); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + + if (diskCtx.hasChangeHandler() && netFile.getFullName() != null) + { + + // Get the change handler + + NotifyChangeHandler changeHandler = diskCtx.getChangeHandler(); + + // Check for file attributes and last write time changes + + if (finfo != null) + { + + // File attributes changed + + if (finfo.hasSetFlag(FileInfo.SetAttributes)) + changeHandler.notifyAttributesChanged(netFile.getFullName(), netFile.isDirectory()); + + // Last write time changed + + if (finfo.hasSetFlag(FileInfo.SetModifyDate)) + changeHandler.notifyLastWriteTimeChanged(netFile.getFullName(), netFile.isDirectory()); + } + else if (infoLevl == FileInfoLevel.SetAllocationInfo || infoLevl == FileInfoLevel.SetEndOfFileInfo) + { + + // File size changed + + changeHandler.notifyFileSizeChanged(netFile.getFullName()); + } + } + } + catch (FileNotFoundException ex) + { + + // Requested file does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to change file attributes/settings + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (DiskFullException ex) + { + + // Disk is full + + m_sess.sendErrorResponseSMB(SMBStatus.NTDiskFull, SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + } + + /** + * Process a transact2 set path information request. + * + * @param tbuf Transaction request details + * @param outPkt SMBSrvPacket + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException SMB protocol exception + */ + protected final void procTrans2SetPath(SrvTransactBuffer tbuf, SMBSrvPacket outPkt) throws java.io.IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the path and information level + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevl = paramBuf.getShort(); + paramBuf.skipBytes(4); + + String path = paramBuf.getString(tbuf.isUnicode()); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug("Set Path - path=" + path + ", level=0x" + Integer.toHexString(infoLevl)); + + // Access the shared device disk interface + + try + { + + // Access the disk interface + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Process the set file information request + + DataBuffer dataBuf = tbuf.getDataBuffer(); + FileInfo finfo = null; + + switch (infoLevl) + { + + // Set standard file information (dates/attributes) + + case FileInfoLevel.SetStandard: + + // Create the file information template + + int setFlags = 0; + finfo = new FileInfo(path, 0, -1); + + // Set the creation date/time, if specified + + int smbDate = dataBuf.getShort(); + int smbTime = dataBuf.getShort(); + + boolean hasSetTime = false; + + if (smbDate != 0 && smbTime != 0) + { + finfo.setCreationDateTime(new SMBDate(smbDate, smbTime).getTime()); + setFlags += FileInfo.SetCreationDate; + hasSetTime = true; + } + + // Set the last access date/time, if specified + + smbDate = dataBuf.getShort(); + smbTime = dataBuf.getShort(); + + if (smbDate != 0 && smbTime != 0) + { + finfo.setAccessDateTime(new SMBDate(smbDate, smbTime).getTime()); + setFlags += FileInfo.SetAccessDate; + hasSetTime = true; + } + + // Set the last write date/time, if specified + + smbDate = dataBuf.getShort(); + smbTime = dataBuf.getShort(); + + if (smbDate != 0 && smbTime != 0) + { + finfo.setModifyDateTime(new SMBDate(smbDate, smbTime).getTime()); + setFlags += FileInfo.SetModifyDate; + hasSetTime = true; + } + + // Set the file size/allocation size + + int fileSize = dataBuf.getInt(); + if (fileSize != 0) + { + finfo.setFileSize(fileSize); + setFlags += FileInfo.SetFileSize; + } + + fileSize = dataBuf.getInt(); + if (fileSize != 0) + { + finfo.setAllocationSize(fileSize); + setFlags += FileInfo.SetAllocationSize; + } + + // Set the attributes + + int attr = dataBuf.getInt(); + int eaListLen = dataBuf.getInt(); + + if (hasSetTime == false && eaListLen == 0) + { + finfo.setFileAttributes(attr); + setFlags += FileInfo.SetAttributes; + } + + // Set the file information for the specified file/directory + + finfo.setFileInformationFlags(setFlags); + disk.setFileInformation(m_sess, conn, path, finfo); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_INFO)) + logger.debug(" Set Standard Info [" + treeId + "] name=" + path + ", attr=0x" + + Integer.toHexString(attr) + ", setTime=" + hasSetTime + ", setFlags=0x" + + Integer.toHexString(setFlags) + ", eaListLen=" + eaListLen); + break; + } + + // Set the return parameter count, so that the data area position can be calculated. + + outPkt.setParameterCount(10); + + // Pack the return information into the data area of the transaction reply + + byte[] buf = outPkt.getBuffer(); + int prmPos = outPkt.getByteOffset(); + + // Longword align the parameters, return an unknown word parameter + // + // Note: Make sure the data offset is on a longword boundary, NT has problems if this is + // not done + + prmPos = DataPacker.longwordAlign(prmPos); + DataPacker.putIntelShort(0, buf, prmPos); + + SMBSrvTransPacket.initTransactReply(outPkt, 2, prmPos, 0, prmPos + 4); + outPkt.setByteCount((prmPos - outPkt.getByteOffset()) + 4); + + // Send the transact reply + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + + if (diskCtx.hasChangeHandler() && path != null) + { + + // Get the change handler + + NotifyChangeHandler changeHandler = diskCtx.getChangeHandler(); + + // Check for file attributes and last write time changes + + if (finfo != null) + { + + // Check if the path refers to a file or directory + + int fileSts = disk.fileExists(m_sess, conn, path); + + // File attributes changed + + if (finfo.hasSetFlag(FileInfo.SetAttributes)) + changeHandler.notifyAttributesChanged(path, fileSts == FileStatus.DirectoryExists ? true + : false); + + // Last write time changed + + if (finfo.hasSetFlag(FileInfo.SetModifyDate)) + changeHandler.notifyLastWriteTimeChanged(path, fileSts == FileStatus.DirectoryExists ? true + : false); + } + } + } + catch (FileNotFoundException ex) + { + + // Requested file does not exist + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Not allowed to change file attributes/settings + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (DiskFullException ex) + { + + // Disk is full + + m_sess.sendErrorResponseSMB(SMBStatus.NTDiskFull, SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + } + + /** + * Process the file write request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procWriteAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid write andX request + + if (m_smbPkt.checkPacketIsValid(12, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + + // Extract the write file parameters + + int fid = m_smbPkt.getParameter(2); + long offset = (long) (((long) m_smbPkt.getParameterLong(3)) & 0xFFFFFFFFL); // bottom 32bits + // of file + // offset + int dataPos = m_smbPkt.getParameter(11) + RFCNetBIOSProtocol.HEADER_LEN; + + int dataLen = m_smbPkt.getParameter(10); + int dataLenHigh = 0; + + if (m_smbPkt.getReceivedLength() > 0xFFFF) + dataLenHigh = m_smbPkt.getParameter(9) & 0x0001; + + if (dataLenHigh > 0) + dataLen += (dataLenHigh << 16); + + // Check for the NT format request that has the top 32bits of the file offset + + if (m_smbPkt.getParameterCount() == 14) + { + long topOff = (long) (((long) m_smbPkt.getParameterLong(12)) & 0xFFFFFFFFL); + offset += topOff << 32; + } + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write AndX [" + netFile.getFileId() + "] : Size=" + dataLen + " ,Pos=" + offset); + + // Write data to the file + + byte[] buf = m_smbPkt.getBuffer(); + int wrtlen = 0; + + // Access the disk interface and write to the file + + try + { + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = (DiskInterface) conn.getSharedDevice().getInterface(); + + // Write to the file + + wrtlen = disk.writeFile(m_sess, conn, netFile, buf, dataPos, dataLen, offset); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Not allowed to write to the file + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (LockConflictException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_LOCK)) + logger.debug("Write Lock Error [" + netFile.getFileId() + "] : Size=" + dataLen + " ,Pos=" + offset); + + // File is locked + + m_sess.sendErrorResponseSMB(SMBStatus.NTLockConflict, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (DiskFullException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("Write Quota Error [" + netFile.getFileId() + "] Disk full : Size=" + dataLen + " ,Pos=" + + offset); + + // Disk is full + + m_sess.sendErrorResponseSMB(SMBStatus.NTDiskFull, SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + catch (java.io.IOException ex) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILEIO)) + logger.debug("File Write Error [" + netFile.getFileId() + "] : " + ex.toString()); + + // Failed to read the file + + m_sess.sendErrorResponseSMB(SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } + + // Return the count of bytes actually written + + outPkt.setParameterCount(6); + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); // AndX offset + outPkt.setParameter(2, wrtlen); + outPkt.setParameter(3, 0xFFFF); + + if (dataLenHigh > 0) + { + outPkt.setParameter(4, dataLen >> 16); + outPkt.setParameter(5, 0); + } + else + { + outPkt.setParameterLong(4, 0); + } + + outPkt.setByteCount(0); + outPkt.setParameter(1, outPkt.getLength()); + + // Send the write response + + m_sess.sendResponseSMB(outPkt); + + // Report file size change notifications every so often + // + // We do not report every write due to the increased overhead of change notifications + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + + if (netFile.getWriteCount() % FileSizeChangeRate == 0 && diskCtx.hasChangeHandler() + && netFile.getFullName() != null) + { + + // Get the change handler + + NotifyChangeHandler changeHandler = diskCtx.getChangeHandler(); + + // File size changed + + changeHandler.notifyFileSizeChanged(netFile.getFullName()); + } + } + + /** + * Process the file create/open request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procNTCreateAndX(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid NT create andX request + + if (m_smbPkt.checkPacketIsValid(24, 1) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is to the IPC$ remote admin named pipe pass the request to the IPC + // handler. If the device is + // not a disk type device then return an error. + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + + // Use the IPC$ handler to process the request + + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + else if (conn.getSharedDevice().getType() != ShareType.DISK) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Extract the NT create andX parameters + + NTParameterPacker prms = new NTParameterPacker(m_smbPkt.getBuffer(), SMBSrvPacket.PARAMWORDS + 5); + + int nameLen = prms.unpackWord(); + int flags = prms.unpackInt(); + int rootFID = prms.unpackInt(); + int accessMask = prms.unpackInt(); + long allocSize = prms.unpackLong(); + int attrib = prms.unpackInt(); + int shrAccess = prms.unpackInt(); + int createDisp = prms.unpackInt(); + int createOptn = prms.unpackInt(); + int impersonLev = prms.unpackInt(); + int secFlags = prms.unpackByte(); + + // Extract the filename string + + String fileName = DataPacker.getUnicodeString(m_smbPkt.getBuffer(), DataPacker.wordAlign(m_smbPkt + .getByteOffset()), nameLen / 2); + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = null; + try + { + + // Get the disk interface for the share + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Check if the file name contains a file stream name. If the disk interface does not + // implement the optional NTFS + // streams interface then return an error status, not supported. + + if ( FileName.containsStreamName(fileName)) + { + + // Check if the driver implements the NTFS streams interface and it is enabled + + boolean streams = false; + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + streams = ntfsStreams.hasStreamsEnabled(m_sess, conn); + } + + // Check if streams are enabled/available + + if (streams == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameInvalid, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + } + + // Create the file open parameters to be passed to the disk interface + + FileOpenParams params = new FileOpenParams(fileName, createDisp, accessMask, attrib, shrAccess, allocSize, + createOptn, rootFID, impersonLev, secFlags); + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("NT Create AndX [" + treeId + "] params=" + params); + + // Access the disk interface and open the requested file + + int fid; + NetworkFile netFile = null; + int respAction = 0; + + try + { + + // Check if the requested file already exists + + int fileSts = disk.fileExists(m_sess, conn, fileName); + + if (fileSts == FileStatus.NotExist) + { + + // Check if the file should be created if it does not exist + + if (createDisp == FileAction.NTCreate || createDisp == FileAction.NTOpenIf + || createDisp == FileAction.NTOverwriteIf || createDisp == FileAction.NTSupersede) + { + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, + SMBStatus.ErrDos); + return; + } + + // Check if a new file or directory should be created + + if ((createOptn & WinNT.CreateDirectory) == 0) + { + + // Create a new file + + netFile = disk.createFile(m_sess, conn, params); + } + else + { + + // Create a new directory and open it + + disk.createDirectory(m_sess, conn, params); + netFile = disk.openFile(m_sess, conn, params); + } + + // Check if the delete on close option is set + + if (netFile != null && (createOptn & WinNT.CreateDeleteOnClose) != 0) + netFile.setDeleteOnClose(true); + + // Indicate that the file did not exist and was created + + respAction = FileAction.FileCreated; + } + else + { + + // Check if the path is a directory + + if (fileSts == FileStatus.DirectoryExists) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + else + { + + // Return a file not found error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, + SMBStatus.ErrDos); + return; + } + } + } + else if (createDisp == FileAction.NTCreate) + { + + // Check for a file or directory + + if (fileSts == FileStatus.FileExists || fileSts == FileStatus.DirectoryExists) + { + + // Return a file exists error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + else + { + + // Return an access denied exception + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + } + else + { + + // Open the requested file/directory + + netFile = disk.openFile(m_sess, conn, params); + + // Check if the file should be truncated + + if (createDisp == FileAction.NTSupersede || createDisp == FileAction.NTOverwriteIf) + { + + // Truncate the file + + disk.truncateFile(m_sess, conn, netFile, 0L); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug(" [" + treeId + "] name=" + fileName + " truncated"); + } + + // Set the file action response + + respAction = FileAction.FileExisted; + } + + // Add the file to the list of open files for this tree connection + + fid = conn.addFile(netFile, getSession()); + + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + m_sess.sendErrorResponseSMB(SMBStatus.NTTooManyOpenFiles, SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (FileExistsException ex) + { + + // File/directory already exists + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + catch (FileSharingException ex) + { + + // Return a sharing violation error + + m_sess.sendErrorResponseSMB(SMBStatus.NTSharingViolation, SMBStatus.DOSFileSharingConflict, + SMBStatus.ErrDos); + return; + } + catch (FileOfflineException ex) + { + + // File data is unavailable + + m_sess.sendErrorResponseSMB(SMBStatus.NTFileOffline, SMBStatus.HRDDriveNotReady, SMBStatus.ErrHrd); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the NT create andX response + + outPkt.setParameterCount((flags & WinNT.ExtendedResponse) != 0 ? 42 : 34); + + outPkt.setAndXCommand(0xFF); + outPkt.setParameter(1, 0); // AndX offset + + prms.reset(outPkt.getBuffer(), SMBSrvPacket.PARAMWORDS + 4); + + // Check if oplocks should be faked + + if (FakeOpLocks) + { + + // If an oplock was requested indicate it was granted, for now + + if ((flags & WinNT.RequestBatchOplock) != 0) + { + + // Batch oplock granted + + prms.packByte(2); + } + else if ((flags & WinNT.RequestOplock) != 0) + { + + // Exclusive oplock granted + + prms.packByte(1); + } + else + { + + // No oplock granted + + prms.packByte(0); + } + } + else + prms.packByte(0); + + // Pack the file id + + prms.packWord(fid); + prms.packInt(respAction); + + // Pack the file/directory dates + + if (netFile.hasCreationDate()) + prms.packLong(NTTime.toNTTime(netFile.getCreationDate())); + else + prms.packLong(0); + + if ( netFile.hasAccessDate()) + prms.packLong(NTTime.toNTTime(netFile.getAccessDate())); + else + prms.packLong(0); + + if (netFile.hasModifyDate()) + { + long modDate = NTTime.toNTTime(netFile.getModifyDate()); + prms.packLong(modDate); + prms.packLong(modDate); + } + else + { + prms.packLong(0); // Last write time + prms.packLong(0); // Change time + } + + prms.packInt(netFile.getFileAttributes()); + + // Pack the file size/allocation size + + long fileSize = netFile.getFileSize(); + if (fileSize > 0L) + fileSize = (fileSize + 512L) & 0xFFFFFFFFFFFFFE00L; + + prms.packLong(fileSize); // Allocation size + prms.packLong(netFile.getFileSize()); // End of file + prms.packWord(0); // File type - disk file + prms.packWord(0); // Device state + prms.packByte(netFile.isDirectory() ? 1 : 0); + + prms.packWord(0); // byte count = 0 + + // Set the AndX offset + + int endPos = prms.getPosition(); + outPkt.setParameter(1, endPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Check if there is a chained request + + if (m_smbPkt.hasAndXCommand()) + { + + // Process the chained requests + + endPos = procAndXCommands(outPkt, endPos, netFile); + } + + // Send the response packet + + m_sess.sendResponseSMB(outPkt, endPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (diskCtx.hasChangeHandler() && respAction == FileAction.FileCreated) + { + + // Check if a file or directory has been created + + if (netFile.isDirectory()) + diskCtx.getChangeHandler().notifyDirectoryChanged(NotifyChange.ActionAdded, fileName); + else + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionAdded, fileName); + } + } + + /** + * Process the cancel request. + * + * @param outPkt SMBSrvPacket + */ + protected final void procNTCancel(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that the received packet looks like a valid NT cancel request + + if (m_smbPkt.checkPacketIsValid(0, 0) == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Find the matching notify request and remove it + + NotifyRequest req = m_sess.findNotifyRequest(m_smbPkt.getMultiplexId(), m_smbPkt.getTreeId(), m_smbPkt + .getUserId(), m_smbPkt.getProcessId()); + if (req != null) + { + + // Remove the request + + m_sess.removeNotifyRequest(req); + + // Return a cancelled status + + m_smbPkt.setParameterCount(0); + m_smbPkt.setByteCount(0); + + // Enable the long error status flag + + if (m_smbPkt.isLongErrorCode() == false) + m_smbPkt.setFlags2(m_smbPkt.getFlags2() + SMBSrvPacket.FLG2_LONGERRORCODE); + + // Set the NT status code + + m_smbPkt.setLongErrorCode(SMBStatus.NTCancelled); + + // Set the Unicode strings flag + + if (m_smbPkt.isUnicode() == false) + m_smbPkt.setFlags2(m_smbPkt.getFlags2() + SMBSrvPacket.FLG2_UNICODE); + + // Return the error response to the client + + m_sess.sendResponseSMB(m_smbPkt); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NOTIFY)) + { + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + logger.debug("NT Cancel notify mid=" + req.getMultiplexId() + ", dir=" + req.getWatchPath() + + ", queue=" + diskCtx.getChangeHandler().getRequestQueueSize()); + } + } + else + { + + // Nothing to cancel + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + } + } + + /** + * Process an NT transaction + * + * @param outPkt SMBSrvPacket + */ + protected final void procNTTransaction(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that we received enough parameters for a transact2 request + + if (m_smbPkt.checkPacketIsValid(19, 0) == false) + { + + // Not enough parameters for a valid transact2 request + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Check if the transaction request is for the IPC$ pipe + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + + // Create an NT transaction using the received packet + + NTTransPacket ntTrans = new NTTransPacket(m_smbPkt.getBuffer()); + int subCmd = ntTrans.getNTFunction(); + + // Check for a notfy change request, this needs special processing + + if (subCmd == PacketType.NTTransNotifyChange) + { + + // Handle the notify change setup request + + procNTTransactNotifyChange(ntTrans, outPkt); + return; + } + + // Create a transact buffer to hold the transaction parameter block and data block + + SrvTransactBuffer transBuf = null; + + if (ntTrans.getTotalParameterCount() == ntTrans.getParameterBlockCount() + && ntTrans.getTotalDataCount() == ntTrans.getDataBlockCount()) + { + + // Create a transact buffer using the packet buffer, the entire request is contained in + // a single + // packet + + transBuf = new SrvTransactBuffer(ntTrans); + } + else + { + + // Create a transact buffer to hold the multiple transact request parameter/data blocks + + transBuf = new SrvTransactBuffer(ntTrans.getSetupCount(), ntTrans.getTotalParameterCount(), ntTrans + .getTotalDataCount()); + transBuf.setType(ntTrans.getCommand()); + transBuf.setFunction(subCmd); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT Transaction [" + treeId + "] transbuf=" + transBuf); + + // Append the setup, parameter and data blocks to the transaction data + + byte[] buf = ntTrans.getBuffer(); + int cnt = ntTrans.getSetupCount(); + + if (cnt > 0) + transBuf.appendSetup(buf, ntTrans.getSetupOffset(), cnt * 2); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT Transaction [" + treeId + "] pcnt=" + ntTrans.getNTParameter(4) + ", offset=" + + ntTrans.getNTParameter(5)); + + cnt = ntTrans.getParameterBlockCount(); + + if (cnt > 0) + transBuf.appendParameter(buf, ntTrans.getParameterBlockOffset(), cnt); + + cnt = ntTrans.getDataBlockCount(); + if (cnt > 0) + transBuf.appendData(buf, ntTrans.getDataBlockOffset(), cnt); + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT Transaction [" + treeId + "] cmd=0x" + Integer.toHexString(subCmd) + ", multiPkt=" + + transBuf.isMultiPacket()); + + // Check for a multi-packet transaction, for a multi-packet transaction we just acknowledge + // the receive with + // an empty response SMB + + if (transBuf.isMultiPacket()) + { + + // Save the partial transaction data + + m_sess.setTransaction(transBuf); + + // Send an intermediate acknowedgement response + + m_sess.sendSuccessResponseSMB(); + return; + } + + // Process the transaction buffer + + processNTTransactionBuffer(transBuf, ntTrans); + } + + /** + * Process an NT transaction secondary packet + * + * @param outPkt SMBSrvPacket + */ + protected final void procNTTransactionSecondary(SMBSrvPacket outPkt) throws java.io.IOException, SMBSrvException + { + + // Check that we received enough parameters for a transact2 request + + if (m_smbPkt.checkPacketIsValid(18, 0) == false) + { + + // Not enough parameters for a valid transact2 request + + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree id from the received packet and validate that it is a valid + // connection id. + + int treeId = m_smbPkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Check if the transaction request is for the IPC$ pipe + + if (conn.getSharedDevice().getType() == ShareType.ADMINPIPE) + { + IPCHandler.processIPCRequest(m_sess, outPkt); + return; + } + + // Check if there is an active transaction, and it is an NT transaction + + if (m_sess.hasTransaction() == false || m_sess.getTransaction().isType() != PacketType.NTTransact) + { + + // No NT transaction to continue, return an error + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Create an NT transaction using the received packet + + NTTransPacket ntTrans = new NTTransPacket(m_smbPkt.getBuffer()); + byte[] buf = ntTrans.getBuffer(); + SrvTransactBuffer transBuf = m_sess.getTransaction(); + + // Append the parameter data to the transaction buffer, if any + + int plen = ntTrans.getParameterBlockCount(); + if (plen > 0) + { + + // Append the data to the parameter buffer + + DataBuffer paramBuf = transBuf.getParameterBuffer(); + paramBuf.appendData(buf, ntTrans.getParameterBlockOffset(), plen); + } + + // Append the data block to the transaction buffer, if any + + int dlen = ntTrans.getDataBlockCount(); + if (dlen > 0) + { + + // Append the data to the data buffer + + DataBuffer dataBuf = transBuf.getDataBuffer(); + dataBuf.appendData(buf, ntTrans.getDataBlockOffset(), dlen); + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT Transaction Secondary [" + treeId + "] paramLen=" + plen + ", dataLen=" + dlen); + + // Check if the transaction has been received or there are more sections to be received + + int totParam = ntTrans.getTotalParameterCount(); + int totData = ntTrans.getTotalDataCount(); + + int paramDisp = ntTrans.getParameterBlockDisplacement(); + int dataDisp = ntTrans.getDataBlockDisplacement(); + + if ((paramDisp + plen) == totParam && (dataDisp + dlen) == totData) + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT Transaction complete, processing ..."); + + // Clear the in progress transaction + + m_sess.setTransaction(null); + + // Process the transaction + + processNTTransactionBuffer(transBuf, ntTrans); + } + + // No response is sent for a transaction secondary + } + + /** + * Process an NT transaction buffer + * + * @param tbuf TransactBuffer + * @param outPkt NTTransPacket + * @exception IOException If a network error occurs + * @exception SMBSrvException If an SMB error occurs + */ + private final void processNTTransactionBuffer(SrvTransactBuffer tbuf, NTTransPacket outPkt) throws IOException, + SMBSrvException + { + + // Process the NT transaction buffer + + switch (tbuf.getFunction()) + { + + // Create file/directory + + case PacketType.NTTransCreate: + procNTTransactCreate(tbuf, outPkt); + break; + + // I/O control + + case PacketType.NTTransIOCtl: + procNTTransactIOCtl(tbuf, outPkt); + break; + + // Query security descriptor + + case PacketType.NTTransQuerySecurityDesc: + procNTTransactQuerySecurityDesc(tbuf, outPkt); + break; + + // Set security descriptor + + case PacketType.NTTransSetSecurityDesc: + procNTTransactSetSecurityDesc(tbuf, outPkt); + break; + + // Rename file/directory via handle + + case PacketType.NTTransRename: + procNTTransactRename(tbuf, outPkt); + break; + + // Get user quota + + case PacketType.NTTransGetUserQuota: + + // Return a not implemented error status + + m_sess.sendErrorResponseSMB(SMBStatus.NTNotImplemented, SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + break; + + // Set user quota + + case PacketType.NTTransSetUserQuota: + + // Return a not implemented error status + + m_sess.sendErrorResponseSMB(SMBStatus.NTNotImplemented, SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + break; + + // Unknown NT transaction command + + default: + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + break; + } + } + + /** + * Process an NT create file/directory transaction + * + * @param tbuf TransactBuffer + * @param outPkt NTTransPacket + * @exception IOException + * @exception SMBSrvException + */ + protected final void procNTTransactCreate(SrvTransactBuffer tbuf, NTTransPacket outPkt) throws IOException, + SMBSrvException + { + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT TransactCreate"); + + // Check that the received packet looks like a valid NT create transaction + + if (tbuf.hasParameterBuffer() && tbuf.getParameterBuffer().getLength() < 52) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the tree connection details + + int treeId = tbuf.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // If the connection is not a disk share then return an error. + + if (conn.getSharedDevice().getType() != ShareType.DISK) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Extract the file create parameters + + DataBuffer tparams = tbuf.getParameterBuffer(); + + int flags = tparams.getInt(); + int rootFID = tparams.getInt(); + int accessMask = tparams.getInt(); + long allocSize = tparams.getLong(); + int attrib = tparams.getInt(); + int shrAccess = tparams.getInt(); + int createDisp = tparams.getInt(); + int createOptn = tparams.getInt(); + int sdLen = tparams.getInt(); + int eaLen = tparams.getInt(); + int nameLen = tparams.getInt(); + int impersonLev = tparams.getInt(); + int secFlags = tparams.getByte(); + + // Extract the filename string + + tparams.wordAlign(); + String fileName = tparams.getString(nameLen, true); + + if (fileName == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Access the disk interface that is associated with the shared device + + DiskInterface disk = null; + try + { + + // Get the disk interface for the share + + disk = (DiskInterface) conn.getSharedDevice().getInterface(); + } + catch (InvalidDeviceInterfaceException ex) + { + + // Failed to get/initialize the disk interface + + m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); + return; + } + + // Check if the file name contains a file stream name. If the disk interface does not + // implement the optional NTFS + // streams interface then return an error status, not supported. + + if (fileName.indexOf(FileOpenParams.StreamSeparator) != -1) + { + + // Check if the driver implements the NTFS streams interface and it is enabled + + boolean streams = false; + + if (disk instanceof NTFSStreamsInterface) + { + + // Check if streams are enabled + + NTFSStreamsInterface ntfsStreams = (NTFSStreamsInterface) disk; + streams = ntfsStreams.hasStreamsEnabled(m_sess, conn); + } + + // Check if streams are enabled/available + + if (streams == false) + { + + // Return a file not found error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + } + + // Create the file open parameters to be passed to the disk interface + + FileOpenParams params = new FileOpenParams(fileName, createDisp, accessMask, attrib, shrAccess, allocSize, + createOptn, rootFID, impersonLev, secFlags); + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("NT TransactCreate [" + treeId + "] params=" + params + " secDescLen=" + sdLen + + ", extAttribLen=" + eaLen); + + // Access the disk interface and open/create the requested file + + int fid; + NetworkFile netFile = null; + int respAction = 0; + + try + { + + // Check if the requested file already exists + + int fileSts = disk.fileExists(m_sess, conn, fileName); + + if (fileSts == FileStatus.NotExist) + { + + // Check if the file should be created if it does not exist + + if (createDisp == FileAction.NTCreate || createDisp == FileAction.NTOpenIf + || createDisp == FileAction.NTOverwriteIf || createDisp == FileAction.NTSupersede) + { + + // Check if a new file or directory should be created + + if ((createOptn & WinNT.CreateDirectory) == 0) + { + + // Create a new file + + netFile = disk.createFile(m_sess, conn, params); + } + else + { + + // Create a new directory and open it + + disk.createDirectory(m_sess, conn, params); + netFile = disk.openFile(m_sess, conn, params); + } + + // Indicate that the file did not exist and was created + + respAction = FileAction.FileCreated; + } + else + { + + // Check if the path is a directory + + if (fileSts == FileStatus.DirectoryExists) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + else + { + + // Return a file not found error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, + SMBStatus.ErrDos); + return; + } + } + } + else if (createDisp == FileAction.NTCreate) + { + + // Check for a file or directory + + if (fileSts == FileStatus.FileExists || fileSts == FileStatus.DirectoryExists) + { + + // Return a file exists error + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + else + { + + // Return an access denied exception + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + } + else + { + + // Open the requested file/directory + + netFile = disk.openFile(m_sess, conn, params); + + // Check if the file should be truncated + + if (createDisp == FileAction.NTSupersede || createDisp == FileAction.NTOverwriteIf) + { + + // Truncate the file + + disk.truncateFile(m_sess, conn, netFile, 0L); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug(" [" + treeId + "] name=" + fileName + " truncated"); + } + + // Set the file action response + + respAction = FileAction.FileExisted; + } + + // Add the file to the list of open files for this tree connection + + fid = conn.addFile(netFile, getSession()); + } + catch (TooManyFilesException ex) + { + + // Too many files are open on this connection, cannot open any more files. + + m_sess.sendErrorResponseSMB(SMBStatus.NTTooManyOpenFiles, SMBStatus.DOSTooManyOpenFiles, SMBStatus.ErrDos); + return; + } + catch (AccessDeniedException ex) + { + + // Return an access denied error + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + catch (FileExistsException ex) + { + + // File/directory already exists + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNameCollision, SMBStatus.DOSFileAlreadyExists, + SMBStatus.ErrDos); + return; + } + catch (FileSharingException ex) + { + + // Return a sharing violation error + + m_sess.sendErrorResponseSMB(SMBStatus.NTSharingViolation, SMBStatus.DOSFileSharingConflict, + SMBStatus.ErrDos); + return; + } + catch (FileOfflineException ex) + { + + // File data is unavailable + + m_sess.sendErrorResponseSMB(SMBStatus.NTFileOffline, SMBStatus.HRDDriveNotReady, SMBStatus.ErrHrd); + return; + } + catch (java.io.IOException ex) + { + + // Failed to open the file + + m_sess.sendErrorResponseSMB(SMBStatus.NTObjectNotFound, SMBStatus.DOSFileNotFound, SMBStatus.ErrDos); + return; + } + + // Build the NT transaction create response + + DataBuffer prms = new DataBuffer(128); + + // If an oplock was requested indicate it was granted, for now + + if ((flags & WinNT.RequestBatchOplock) != 0) + { + + // Batch oplock granted + + prms.putByte(2); + } + else if ((flags & WinNT.RequestOplock) != 0) + { + + // Exclusive oplock granted + + prms.putByte(1); + } + else + { + + // No oplock granted + + prms.putByte(0); + } + prms.putByte(0); // alignment + + // Pack the file id + + prms.putShort(fid); + prms.putInt(respAction); + + // EA error offset + + prms.putInt(0); + + // Pack the file/directory dates + + if (netFile.hasCreationDate()) + prms.putLong(NTTime.toNTTime(netFile.getCreationDate())); + else + prms.putLong(0); + + if (netFile.hasModifyDate()) + { + long modDate = NTTime.toNTTime(netFile.getModifyDate()); + prms.putLong(modDate); + prms.putLong(modDate); + prms.putLong(modDate); + } + else + { + prms.putLong(0); // Last access time + prms.putLong(0); // Last write time + prms.putLong(0); // Change time + } + + prms.putInt(netFile.getFileAttributes()); + + // Pack the file size/allocation size + + prms.putLong(netFile.getFileSize()); // Allocation size + prms.putLong(netFile.getFileSize()); // End of file + prms.putShort(0); // File type - disk file + prms.putShort(0); // Device state + prms.putByte(netFile.isDirectory() ? 1 : 0); + + // Initialize the transaction response + + outPkt.initTransactReply(prms.getBuffer(), prms.getLength(), null, 0); + + // Send back the response + + m_sess.sendResponseSMB(outPkt); + + // Check if there are any file/directory change notify requests active + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (diskCtx.hasChangeHandler() && respAction == FileAction.FileCreated) + { + + // Check if a file or directory has been created + + if (netFile.isDirectory()) + diskCtx.getChangeHandler().notifyDirectoryChanged(NotifyChange.ActionAdded, fileName); + else + diskCtx.getChangeHandler().notifyFileChanged(NotifyChange.ActionAdded, fileName); + } + } + + /** + * Process an NT I/O control transaction + * + * @param tbuf TransactBuffer + * @param outPkt NTTransPacket + * @exception IOException + * @exception SMBSrvException + */ + protected final void procNTTransactIOCtl(SrvTransactBuffer tbuf, NTTransPacket outPkt) throws IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = tbuf.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Send back an error, IOctl not supported + + m_sess.sendErrorResponseSMB(SMBStatus.NTNotImplemented, SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + + /** + * Process an NT query security descriptor transaction + * + * @param tbuf TransactBuffer + * @param outPkt NTTransPacket + * @exception IOException + * @exception SMBSrvException + */ + protected final void procNTTransactQuerySecurityDesc(SrvTransactBuffer tbuf, NTTransPacket outPkt) + throws IOException, SMBSrvException + { + + // Get the tree connection details + + int treeId = tbuf.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Unpack the request details + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int fid = paramBuf.getShort(); + int flags = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT QuerySecurityDesc fid=" + fid + ", flags=" + flags); + + // Get the file details + + NetworkFile netFile = conn.findFile(fid); + + if (netFile == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if this is a buffer length check, if so the maximum returned data count will be + // zero + + if (tbuf.getReturnDataLimit() == 0) + { + + // Return the security descriptor length in the parameter block + + byte[] paramblk = new byte[4]; + DataPacker.putIntelInt(_sdEveryOne.length, paramblk, 0); + + // Initialize the transaction reply + + outPkt.initTransactReply(paramblk, paramblk.length, null, 0); + + // Set a warning status to indicate the supplied data buffer was too small to return the + // security + // descriptor + + outPkt.setLongErrorCode(SMBStatus.NTBufferTooSmall); + } + else + { + + // Return the security descriptor length in the parameter block + + byte[] paramblk = new byte[4]; + DataPacker.putIntelInt(_sdEveryOne.length, paramblk, 0); + + // Initialize the transaction reply. Return the fixed security descriptor that allows + // anyone to access the + // file/directory + + outPkt.initTransactReply(paramblk, paramblk.length, _sdEveryOne, _sdEveryOne.length); + } + + // Send back the response + + m_sess.sendResponseSMB(outPkt); + } + + /** + * Process an NT set security descriptor transaction + * + * @param tbuf TransactBuffer + * @param outPkt NTTransPacket + * @exception IOException + * @exception SMBSrvException + */ + protected final void procNTTransactSetSecurityDesc(SrvTransactBuffer tbuf, NTTransPacket outPkt) + throws IOException, SMBSrvException + { + + // Unpack the request details + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + // Get the tree connection details + + int treeId = tbuf.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Get the file details + + int fid = paramBuf.getShort(); + paramBuf.skipBytes(2); + int flags = paramBuf.getInt(); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT SetSecurityDesc fid=" + fid + ", flags=" + flags); + + // Send back an error, security descriptors not supported + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + } + + /** + * Process an NT change notification transaction + * + * @param ntpkt NTTransPacket + * @param outPkt SMBSrvPacket + * @exception IOException + * @exception SMBSrvException + */ + protected final void procNTTransactNotifyChange(NTTransPacket ntpkt, SMBSrvPacket outPkt) throws IOException, + SMBSrvException + { + + // Get the tree connection details + + int treeId = ntpkt.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasReadAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Make sure the tree connection is for a disk device + + if (conn.getContext() == null || conn.getContext() instanceof DiskDeviceContext == false) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Check if the device has change notification enabled + + DiskDeviceContext diskCtx = (DiskDeviceContext) conn.getContext(); + if (diskCtx.hasChangeHandler() == false) + { + + // Return an error status, share does not have change notification enabled + + m_sess.sendErrorResponseSMB(SMBStatus.NTNotImplemented, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Unpack the request details + + ntpkt.resetSetupPointer(); + + int filter = ntpkt.unpackInt(); + int fid = ntpkt.unpackWord(); + boolean watchTree = ntpkt.unpackByte() == 1 ? true : false; + int mid = ntpkt.getMultiplexId(); + + // Get the file details + + NetworkFile dir = conn.findFile(fid); + if (dir == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + return; + } + + // Get the maximum notifications to buffer whilst waiting for the request to be reset after + // a notification + // has been triggered + + int maxQueue = 0; + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NOTIFY)) + logger.debug("NT NotifyChange fid=" + fid + ", mid=" + mid + ", filter=0x" + Integer.toHexString(filter) + + ", dir=" + dir.getFullName() + ", maxQueue=" + maxQueue); + + // Check if there is an existing request in the notify list that matches the new request and + // is in a completed + // state. If so then the client is resetting the notify request so reuse the existing + // request. + + NotifyRequest req = m_sess.findNotifyRequest(dir, filter, watchTree); + + if (req != null && req.isCompleted()) + { + + // Reset the existing request with the new multiplex id + + req.setMultiplexId(mid); + req.setCompleted(false); + + // Check if there are any buffered notifications for this session + + if (req.hasBufferedEvents() || req.hasNotifyEnum()) + { + + // Get the buffered events from the request, clear the list from the request + + NotifyChangeEventList bufList = req.getBufferedEventList(); + req.clearBufferedEvents(); + + // Send the buffered events + + diskCtx.getChangeHandler().sendBufferedNotifications(req, bufList); + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NOTIFY)) + { + if (bufList == null) + logger.debug(" Sent buffered notifications, req=" + req.toString() + ", Enum"); + else + logger.debug(" Sent buffered notifications, req=" + req.toString() + ", count=" + + bufList.numberOfEvents()); + } + } + else + { + + // DEBUG + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NOTIFY)) + logger.debug(" Reset notify request, " + req.toString()); + } + } + else + { + + // Create a change notification request + + req = new NotifyRequest(filter, watchTree, m_sess, dir, mid, ntpkt.getTreeId(), ntpkt.getProcessId(), ntpkt + .getUserId(), maxQueue); + + // Add the request to the pending notify change lists + + m_sess.addNotifyRequest(req, diskCtx); + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_NOTIFY)) + logger.debug(" Added new request, " + req.toString()); + } + + // NOTE: If the change notification request is accepted then no reply is sent to the client. + // A reply will be sent + // asynchronously if the change notification is triggered. + } + + /** + * Process an NT rename via handle transaction + * + * @param tbuf TransactBuffer + * @param outPkt NTTransPacket + * @exception IOException + * @exception SMBSrvException + */ + protected final void procNTTransactRename(SrvTransactBuffer tbuf, NTTransPacket outPkt) throws IOException, + SMBSrvException + { + + // Unpack the request details + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + // Get the tree connection details + + int treeId = tbuf.getTreeId(); + TreeConnection conn = m_sess.findConnection(treeId); + + if (conn == null) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTInvalidParameter, SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos); + return; + } + + // Check if the user has the required access permission + + if (conn.hasWriteAccess() == false) + { + + // User does not have the required access rights + + m_sess.sendErrorResponseSMB(SMBStatus.NTAccessDenied, SMBStatus.DOSAccessDenied, SMBStatus.ErrDos); + return; + } + + // Debug + + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_TRAN)) + logger.debug("NT TransactRename"); + + // Send back an error, NT rename not supported + + m_sess.sendErrorResponseSMB(SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/NTTransPacket.java b/source/java/org/alfresco/filesys/smb/server/NTTransPacket.java new file mode 100644 index 0000000000..2c7b3bbdf3 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/NTTransPacket.java @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.util.DataPacker; + +/** + * NT Transaction Packet Class + */ +public class NTTransPacket extends SMBSrvPacket +{ + + // Define the number of standard parameter words/bytes + + private static final int StandardParams = 19; + private static final int ParameterBytes = 36; // 8 x 32bit params + max setup count byte + + // setup count byte + reserved word + + // Standard reply word count + + private static final int ReplyParams = 18; + + // Offset to start of NT parameters from start of packet + + private static final int NTMaxSetupCount = SMBPacket.PARAMWORDS; + private static final int NTParams = SMBPacket.PARAMWORDS + 3; + private static final int NTSetupCount = NTParams + 32; + private static final int NTFunction = NTSetupCount + 1; + + // Default return parameter/data byte counts + + private static final int DefaultReturnParams = 4; + private static final int DefaultReturnData = 1024; + + /** + * Default constructor + */ + public NTTransPacket() + { + super(); + } + + /** + * Class constructor + * + * @param buf byte[] + */ + public NTTransPacket(byte[] buf) + { + super(buf); + } + + /** + * Copy constructor + * + * @param pkt NTTransPacket + */ + public NTTransPacket(NTTransPacket pkt) + { + super(pkt); + } + + /** + * Return the data block size + * + * @return Data block size in bytes + */ + public final int getDataLength() + { + return getNTParameter(6); + } + + /** + * Return the data block offset + * + * @return Data block offset within the SMB packet. + */ + public final int getDataOffset() + { + return getNTParameter(7) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Unpack the parameter block + * + * @return int[] + */ + public final int[] getParameterBlock() + { + + // Get the parameter count and allocate the parameter buffer + + int prmcnt = getParameterBlockCount() / 4; // convert to number of ints + if (prmcnt <= 0) + return null; + int[] prmblk = new int[prmcnt]; + + // Get the offset to the parameter words, add the NetBIOS header length + // to the offset. + + int pos = getParameterBlockOffset(); + + // Unpack the parameter ints + + setBytePointer(pos, getByteCount()); + + for (int idx = 0; idx < prmcnt; idx++) + { + + // Unpack the current parameter value + + prmblk[idx] = unpackInt(); + } + + // Return the parameter block + + return prmblk; + } + + /** + * Return the total parameter count + * + * @return int + */ + public final int getTotalParameterCount() + { + return getNTParameter(0); + } + + /** + * Return the total data count + * + * @return int + */ + public final int getTotalDataCount() + { + return getNTParameter(1); + } + + /** + * Return the maximum parameter block length to be returned + * + * @return int + */ + public final int getMaximumParameterReturn() + { + return getNTParameter(2); + } + + /** + * Return the maximum data block length to be returned + * + * @return int + */ + public final int getMaximumDataReturn() + { + return getNTParameter(3); + } + + /** + * Return the parameter block count + * + * @return int + */ + public final int getParameterBlockCount() + { + return getNTParameter(getCommand() == PacketType.NTTransact ? 4 : 2); + } + + /** + * Return the parameter block offset + * + * @return int + */ + public final int getParameterBlockOffset() + { + return getNTParameter(getCommand() == PacketType.NTTransact ? 5 : 3) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the paramater block displacement + * + * @return int + */ + public final int getParameterBlockDisplacement() + { + return getNTParameter(4); + } + + /** + * Return the data block count + * + * @return int + */ + public final int getDataBlockCount() + { + return getNTParameter(getCommand() == PacketType.NTTransact ? 6 : 5); + } + + /** + * Return the data block offset + * + * @return int + */ + public final int getDataBlockOffset() + { + return getNTParameter(getCommand() == PacketType.NTTransact ? 7 : 6) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the data block displacment + * + * @return int + */ + public final int getDataBlockDisplacement() + { + return getNTParameter(7); + } + + /** + * Get an NT parameter (32bit) + * + * @param idx int + * @return int + */ + protected final int getNTParameter(int idx) + { + int pos = NTParams + (4 * idx); + return DataPacker.getIntelInt(getBuffer(), pos); + } + + /** + * Get the setup parameter count + * + * @return int + */ + public final int getSetupCount() + { + byte[] buf = getBuffer(); + return (int) buf[NTSetupCount] & 0xFF; + } + + /** + * Return the offset to the setup words data + * + * @return int + */ + public final int getSetupOffset() + { + return NTFunction + 2; + } + + /** + * Get the NT transaction function code + * + * @return int + */ + public final int getNTFunction() + { + byte[] buf = getBuffer(); + return DataPacker.getIntelShort(buf, NTFunction); + } + + /** + * Initialize the transact SMB packet + * + * @param func NT transaction function code + * @param paramblk Parameter block data bytes + * @param plen Parameter block data length + * @param datablk Data block data bytes + * @param dlen Data block data length + * @param setupcnt Number of setup parameters + */ + public final void initTransact(int func, byte[] paramblk, int plen, byte[] datablk, int dlen, int setupcnt) + { + initTransact(func, paramblk, plen, datablk, dlen, setupcnt, DefaultReturnParams, DefaultReturnData); + } + + /** + * Initialize the transact SMB packet + * + * @param func NT transaction function code + * @param paramblk Parameter block data bytes + * @param plen Parameter block data length + * @param datablk Data block data bytes + * @param dlen Data block data length + * @param setupcnt Number of setup parameters + * @param maxPrm Maximum parameter bytes to return + * @param maxData Maximum data bytes to return + */ + public final void initTransact(int func, byte[] paramblk, int plen, byte[] datablk, int dlen, int setupcnt, + int maxPrm, int maxData) + { + + // Set the SMB command and parameter count + + setCommand(PacketType.NTTransact); + setParameterCount(StandardParams + setupcnt); + + // Initialize the parameters + + setTotalParameterCount(plen); + setTotalDataCount(dlen); + setMaximumParameterReturn(maxPrm); + setMaximumDataReturn(maxData); + setParameterCount(plen); + setParameterBlockOffset(0); + setDataBlockCount(dlen); + setDataBlockOffset(0); + + setSetupCount(setupcnt); + setNTFunction(func); + + resetBytePointerAlign(); + + // Pack the parameter block + + if (paramblk != null) + { + + // Set the parameter block offset, from the start of the SMB packet + + setParameterBlockOffset(getPosition()); + + // Pack the parameter block + + packBytes(paramblk, plen); + } + + // Pack the data block + + if (datablk != null) + { + + // Align the byte area offset and set the data block offset in the request + + alignBytePointer(); + setDataBlockOffset(getPosition()); + + // Pack the data block + + packBytes(datablk, dlen); + } + + // Set the byte count for the SMB packet + + setByteCount(); + } + + /** + * Initialize the NT transaction reply + * + * @param paramblk Parameter block data bytes + * @param plen Parameter block data length + * @param datablk Data block data bytes + * @param dlen Data block data length + */ + public final void initTransactReply(byte[] paramblk, int plen, byte[] datablk, int dlen) + { + + // Set the parameter count + + setParameterCount(ReplyParams); + setSetupCount(0); + + // Initialize the parameters + + setTotalParameterCount(plen); + setTotalDataCount(dlen); + + setReplyParameterCount(plen); + setReplyParameterOffset(0); + setReplyParameterDisplacement(0); + + setReplyDataCount(dlen); + setDataBlockOffset(0); + setReplyDataDisplacement(0); + + setSetupCount(0); + + resetBytePointerAlign(); + + // Pack the parameter block + + if (paramblk != null) + { + + // Set the parameter block offset, from the start of the SMB packet + + setReplyParameterOffset(getPosition() - 4); + + // Pack the parameter block + + packBytes(paramblk, plen); + } + + // Pack the data block + + if (datablk != null) + { + + // Align the byte area offset and set the data block offset in the request + + alignBytePointer(); + setReplyDataOffset(getPosition() - 4); + + // Pack the data block + + packBytes(datablk, dlen); + } + + // Set the byte count for the SMB packet + + setByteCount(); + } + + /** + * Initialize the NT transaction reply + * + * @param paramblk Parameter block data bytes + * @param plen Parameter block data length + * @param datablk Data block data bytes + * @param dlen Data block data length + * @param setupCnt Number of setup parameter + */ + public final void initTransactReply(byte[] paramblk, int plen, byte[] datablk, int dlen, int setupCnt) + { + + // Set the parameter count, add the setup parameter count + + setParameterCount(ReplyParams + setupCnt); + setSetupCount(setupCnt); + + // Initialize the parameters + + setTotalParameterCount(plen); + setTotalDataCount(dlen); + + setReplyParameterCount(plen); + setReplyParameterOffset(0); + setReplyParameterDisplacement(0); + + setReplyDataCount(dlen); + setDataBlockOffset(0); + setReplyDataDisplacement(0); + + setSetupCount(setupCnt); + + resetBytePointerAlign(); + + // Pack the parameter block + + if (paramblk != null) + { + + // Set the parameter block offset, from the start of the SMB packet + + setReplyParameterOffset(getPosition() - 4); + + // Pack the parameter block + + packBytes(paramblk, plen); + } + + // Pack the data block + + if (datablk != null) + { + + // Align the byte area offset and set the data block offset in the request + + alignBytePointer(); + setReplyDataOffset(getPosition() - 4); + + // Pack the data block + + packBytes(datablk, dlen); + } + + // Set the byte count for the SMB packet + + setByteCount(); + } + + /** + * Set the total parameter count + * + * @param cnt int + */ + public final void setTotalParameterCount(int cnt) + { + setNTParameter(0, cnt); + } + + /** + * Set the total data count + * + * @param cnt int + */ + public final void setTotalDataCount(int cnt) + { + setNTParameter(1, cnt); + } + + /** + * Set the maximum return parameter count + * + * @param cnt int + */ + public final void setMaximumParameterReturn(int cnt) + { + setNTParameter(2, cnt); + } + + /** + * Set the maximum return data count + * + * @param cnt int + */ + public final void setMaximumDataReturn(int cnt) + { + setNTParameter(3, cnt); + } + + /** + * Set the paramater block count + * + * @param disp int + */ + public final void setTransactParameterCount(int cnt) + { + setNTParameter(4, cnt); + } + + /** + * Set the reply parameter byte count + * + * @param cnt int + */ + public final void setReplyParameterCount(int cnt) + { + setNTParameter(2, cnt); + } + + /** + * Set the reply parameter offset + * + * @param off int + */ + public final void setReplyParameterOffset(int off) + { + setNTParameter(3, off); + } + + /** + * Set the reply parameter bytes displacement + * + * @param disp int + */ + public final void setReplyParameterDisplacement(int disp) + { + setNTParameter(4, disp); + } + + /** + * Set the reply data byte count + * + * @param cnt int + */ + public final void setReplyDataCount(int cnt) + { + setNTParameter(5, cnt); + } + + /** + * Set the reply data offset + * + * @param off int + */ + public final void setReplyDataOffset(int off) + { + setNTParameter(6, off); + } + + /** + * Set the reply data bytes displacement + * + * @param disp int + */ + public final void setReplyDataDisplacement(int disp) + { + setNTParameter(7, disp); + } + + /** + * Set the parameter block offset within the packet + * + * @param off int + */ + public final void setParameterBlockOffset(int off) + { + setNTParameter(5, off != 0 ? off - RFCNetBIOSProtocol.HEADER_LEN : 0); + } + + /** + * Set the data block count + * + * @param cnt int + */ + public final void setDataBlockCount(int cnt) + { + setNTParameter(6, cnt); + } + + /** + * Set the data block offset + * + * @param disp int + */ + public final void setDataBlockOffset(int off) + { + setNTParameter(7, off != 0 ? off - RFCNetBIOSProtocol.HEADER_LEN : 0); + } + + /** + * Set an NT parameter (32bit) + * + * @param idx int + * @param val int + */ + public final void setNTParameter(int idx, int val) + { + int pos = NTParams + (4 * idx); + DataPacker.putIntelInt(val, getBuffer(), pos); + } + + /** + * Set the maximum setup parameter count + * + * @param cnt Maximum count of setup paramater words + */ + public final void setMaximumSetupCount(int cnt) + { + byte[] buf = getBuffer(); + buf[NTMaxSetupCount] = (byte) cnt; + } + + /** + * Set the setup parameter count + * + * @param cnt Count of setup paramater words + */ + public final void setSetupCount(int cnt) + { + byte[] buf = getBuffer(); + buf[NTSetupCount] = (byte) cnt; + } + + /** + * Set the specified setup parameter + * + * @param setupIdx Setup parameter index + * @param setupVal Setup parameter value + */ + public final void setSetupParameter(int setupIdx, int setupVal) + { + int pos = NTSetupCount + 1 + (setupIdx * 2); + DataPacker.putIntelShort(setupVal, getBuffer(), pos); + } + + /** + * Set the NT transaction function code + * + * @param func int + */ + public final void setNTFunction(int func) + { + byte[] buf = getBuffer(); + DataPacker.putIntelShort(func, buf, NTFunction); + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking setup paramaters items to the + * packet + */ + public final void resetSetupPointer() + { + m_pos = NTFunction + 2; + m_endpos = m_pos; + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking the transaction data block + */ + public final void resetDataBlockPointer() + { + m_pos = getDataBlockOffset(); + m_endpos = m_pos; + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking the transaction paramater block + */ + public final void resetParameterBlockPointer() + { + m_pos = getParameterBlockOffset(); + m_endpos = m_pos; + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/NamedPipeTransaction.java b/source/java/org/alfresco/filesys/smb/server/NamedPipeTransaction.java new file mode 100644 index 0000000000..b50f546b43 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/NamedPipeTransaction.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +/** + *

    + * Contains the named pipe transaction codes. + */ +public class NamedPipeTransaction +{ + + // Transaction sub-commands + + public static final int CallNamedPipe = 0x54; + public static final int WaitNamedPipe = 0x53; + public static final int PeekNmPipe = 0x23; + public static final int QNmPHandState = 0x21; + public static final int SetNmPHandState = 0x01; + public static final int QNmPipeInfo = 0x22; + public static final int TransactNmPipe = 0x26; + public static final int RawReadNmPipe = 0x11; + public static final int RawWriteNmPipe = 0x31; + + /** + * Return the named pipe transaction sub-command as a string + * + * @param subCmd int + * @return String + */ + public final static String getSubCommand(int subCmd) + { + + // Determine the sub-command code + + String ret = ""; + + switch (subCmd) + { + case CallNamedPipe: + ret = "CallNamedPipe"; + break; + case WaitNamedPipe: + ret = "WaitNamedPipe"; + break; + case PeekNmPipe: + ret = "PeekNmPipe"; + break; + case QNmPHandState: + ret = "QNmPHandState"; + break; + case SetNmPHandState: + ret = "SetNmPHandState"; + break; + case QNmPipeInfo: + ret = "QNmPipeInfo"; + break; + case TransactNmPipe: + ret = "TransactNmPipe"; + break; + case RawReadNmPipe: + ret = "RawReadNmPipe"; + break; + case RawWriteNmPipe: + ret = "RawWriteNmPipe"; + break; + } + return ret; + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/NetBIOSPacketHandler.java b/source/java/org/alfresco/filesys/smb/server/NetBIOSPacketHandler.java new file mode 100644 index 0000000000..e256d12e32 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/NetBIOSPacketHandler.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; +import java.net.Socket; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.util.DataPacker; + +/** + * NetBIOS Protocol Packet Handler Class + */ +public class NetBIOSPacketHandler extends PacketHandler +{ + + /** + * Class constructor + * + * @param sock Socket + * @exception IOException If a network error occurs + */ + public NetBIOSPacketHandler(Socket sock) throws IOException + { + super(sock, SMBSrvPacket.PROTOCOL_NETBIOS, "NetBIOS", "NB"); + } + + /** + * Read a packet from the input stream + * + * @param pkt SMBSrvPacket + * @return int + * @exception IOexception If a network error occurs + */ + public final int readPacket(SMBSrvPacket pkt) throws IOException + { + + // Read the packet header + + byte[] buf = pkt.getBuffer(); + int len = 0; + + while (len < RFCNetBIOSProtocol.HEADER_LEN && len != -1) + len = readPacket(buf, len, RFCNetBIOSProtocol.HEADER_LEN - len); + + // Check if the connection has been closed, read length equals -1 + + if (len == -1) + return len; + + // Check if we received a valid NetBIOS header + + if (len < RFCNetBIOSProtocol.HEADER_LEN) + throw new IOException("Invalid NetBIOS header, len=" + len); + + // Get the packet type from the header + + int typ = (int) (buf[0] & 0xFF); + int flags = (int) buf[1]; + int dlen = (int) DataPacker.getShort(buf, 2); + + if ((flags & 0x01) != 0) + dlen += 0x10000; + + // Check for a session keep alive type message + + if (typ == RFCNetBIOSProtocol.SESSION_KEEPALIVE) + return 0; + + // Check if the packet buffer is large enough to hold the data + header + + if (buf.length < (dlen + RFCNetBIOSProtocol.HEADER_LEN)) + { + + // Allocate a new buffer to hold the data and copy the existing header + + byte[] newBuf = new byte[dlen + RFCNetBIOSProtocol.HEADER_LEN]; + for (int i = 0; i < 4; i++) + newBuf[i] = buf[i]; + + // Attach the new buffer to the SMB packet + + pkt.setBuffer(newBuf); + buf = newBuf; + } + + // Read the data part of the packet into the users buffer, this may take + // several reads + + int offset = RFCNetBIOSProtocol.HEADER_LEN; + int totlen = offset; + + while (dlen > 0) + { + + // Read the data + + len = readPacket(buf, offset, dlen); + + // Check if the connection has been closed + + if (len == -1) + return -1; + + // Update the received length and remaining data length + + totlen += len; + dlen -= len; + + // Update the user buffer offset as more reads will be required + // to complete the data read + + offset += len; + + } // end while reading data + + // Return the received packet length + + return totlen; + } + + /** + * Send a packet to the output stream + * + * @param pkt SMBSrvPacket + * @param len int + * @exception IOexception If a network error occurs + */ + public final void writePacket(SMBSrvPacket pkt, int len) throws IOException + { + + // Fill in the NetBIOS message header, this is already allocated as + // part of the users buffer. + + byte[] buf = pkt.getBuffer(); + buf[0] = (byte) RFCNetBIOSProtocol.SESSION_MESSAGE; + buf[1] = (byte) 0; + + if (len > 0xFFFF) + { + + // Set the >64K flag + + buf[1] = (byte) 0x01; + + // Set the low word of the data length + + DataPacker.putShort((short) (len & 0xFFFF), buf, 2); + } + else + { + + // Set the data length + + DataPacker.putShort((short) len, buf, 2); + } + + // Output the data packet + + int bufSiz = len + RFCNetBIOSProtocol.HEADER_LEN; + writePacket(buf, 0, bufSiz); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/NetBIOSSessionSocketHandler.java b/source/java/org/alfresco/filesys/smb/server/NetBIOSSessionSocketHandler.java new file mode 100644 index 0000000000..0e85b2d248 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/NetBIOSSessionSocketHandler.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.smb.mailslot.TcpipNetBIOSHostAnnouncer; + +/** + * NetBIOS Socket Session Handler Class + */ +public class NetBIOSSessionSocketHandler extends SessionSocketHandler +{ + + /** + * Class constructor + * + * @param srv SMBServer + * @param port int + * @param bindAddr InetAddress + * @param debug boolean + */ + public NetBIOSSessionSocketHandler(SMBServer srv, int port, InetAddress bindAddr, boolean debug) + { + super("NetBIOS", srv, port, bindAddr, debug); + } + + /** + * Run the NetBIOS session socket handler + */ + public void run() + { + + try + { + + // Clear the shutdown flag + + clearShutdown(); + + // Wait for incoming connection requests + + while (hasShutdown() == false) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Waiting for NetBIOS session request ..."); + + // Wait for a connection + + Socket sessSock = getSocket().accept(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] NetBIOS session request received from " + + sessSock.getInetAddress().getHostAddress()); + + try + { + + // Create a packet handler for the session + + PacketHandler pktHandler = new NetBIOSPacketHandler(sessSock); + + // Create a server session for the new request, and set the session id. + + SMBSrvSession srvSess = new SMBSrvSession(pktHandler, getServer()); + srvSess.setSessionId(getNextSessionId()); + srvSess.setUniqueId(pktHandler.getShortName() + srvSess.getSessionId()); + srvSess.setDebugPrefix("[" + pktHandler.getShortName() + srvSess.getSessionId() + "] "); + + // Add the session to the active session list + + getServer().addSession(srvSess); + + // Start the new session in a seperate thread + + Thread srvThread = new Thread(srvSess); + srvThread.setDaemon(true); + srvThread.setName("Sess_N" + srvSess.getSessionId() + "_" + + sessSock.getInetAddress().getHostAddress()); + srvThread.start(); + } + catch (Exception ex) + { + + // Debug + + logger.error("[SMB] NetBIOS Failed to create session, ", ex); + } + } + } + catch (SocketException ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + logger.error("[SMB] NetBIOS Socket error : ", ex); + } + catch (Exception ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + logger.error("[SMB] NetBIOS Server error : ", ex); + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] NetBIOS session handler closed"); + } + + /** + * Create the TCP/IP NetBIOS session socket handlers for the main SMB/CIFS server + * + * @param server SMBServer + * @param sockDbg boolean + * @exception Exception + */ + public final static void createSessionHandlers(SMBServer server, boolean sockDbg) throws Exception + { + + // Access the server configuration + + ServerConfiguration config = server.getConfiguration(); + + // Create the NetBIOS SMB handler + + SessionSocketHandler sessHandler = new NetBIOSSessionSocketHandler(server, RFCNetBIOSProtocol.PORT, config + .getSMBBindAddress(), sockDbg); + sessHandler.initialize(); + + // Add the session handler to the list of active handlers + + server.addSessionHandler(sessHandler); + + // Run the NetBIOS session handler in a seperate thread + + Thread nbThread = new Thread(sessHandler); + nbThread.setName("NetBIOS_Handler"); + nbThread.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] TCP NetBIOS session handler created"); + + // Check if a host announcer should be created + + if (config.hasEnableAnnouncer()) + { + + // Create the TCP NetBIOS host announcer + + TcpipNetBIOSHostAnnouncer announcer = new TcpipNetBIOSHostAnnouncer(); + + // Set the host name to be announced + + announcer.addHostName(config.getServerName()); + announcer.setDomain(config.getDomainName()); + announcer.setComment(config.getComment()); + announcer.setBindAddress(config.getSMBBindAddress()); + + // Set the announcement interval + + if (config.getHostAnnounceInterval() > 0) + announcer.setInterval(config.getHostAnnounceInterval()); + + try + { + announcer.setBroadcastAddress(config.getBroadcastMask()); + } + catch (Exception ex) + { + } + + // Set the server type flags + + announcer.setServerType(config.getServerType()); + + // Enable debug output + + if (config.hasHostAnnounceDebug()) + announcer.setDebug(true); + + // Add the host announcer to the SMS servers list + + server.addHostAnnouncer(announcer); + + // Start the host announcer thread + + announcer.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] TCP NetBIOS host announcer created"); + } + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/OpenAndX.java b/source/java/org/alfresco/filesys/smb/server/OpenAndX.java new file mode 100644 index 0000000000..88530e926b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/OpenAndX.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +/** + * OpenAndX Flags Class + */ +class OpenAndX +{ + + // File types, for OpenAndX + + protected static final int FileTypeDisk = 0; + protected static final int FileTypeBytePipe = 1; + protected static final int FileTypeMsgPipe = 2; + protected static final int FileTypePrinter = 3; + protected static final int FileTypeUnknown = 0xFFFF; +} diff --git a/source/java/org/alfresco/filesys/smb/server/PacketHandler.java b/source/java/org/alfresco/filesys/smb/server/PacketHandler.java new file mode 100644 index 0000000000..dfd3bf9778 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/PacketHandler.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + +/** + * Protocol Packet Handler Interface + */ +public abstract class PacketHandler +{ + + // Protocol type and name + + private int m_protoType; + private String m_protoName; + private String m_shortName; + + // Socket that this session is using. + + private Socket m_socket; + + // Input/output streams for receiving/sending SMB requests. + + private DataInputStream m_in; + private DataOutputStream m_out; + + // Client caller name + + private String m_clientName; + + /** + * Class constructor + * + * @param sock Socket + * @param typ int + * @param name String + * @param shortName String + * @exception IOException If a network error occurs + */ + public PacketHandler(Socket sock, int typ, String name, String shortName) throws IOException + { + m_socket = sock; + m_protoType = typ; + m_protoName = name; + m_shortName = shortName; + + // Set socket options + + sock.setTcpNoDelay(true); + + // Open the input/output streams + + m_in = new DataInputStream(m_socket.getInputStream()); + m_out = new DataOutputStream(m_socket.getOutputStream()); + } + + /** + * Class constructor + * + * @param typ int + * @param name String + * @param shortName String + */ + public PacketHandler(int typ, String name, String shortName, String clientName) + { + m_protoType = typ; + m_protoName = name; + m_shortName = shortName; + + m_clientName = clientName; + } + + /** + * Return the protocol type + * + * @return int + */ + public final int isProtocol() + { + return m_protoType; + } + + /** + * Return the protocol name + * + * @return String + */ + public final String isProtocolName() + { + return m_protoName; + } + + /** + * Return the short protocol name + * + * @return String + */ + public final String getShortName() + { + return m_shortName; + } + + /** + * Check if there is a remote address available + * + * @return boolean + */ + public final boolean hasRemoteAddress() + { + return m_socket != null ? true : false; + } + + /** + * Return the remote address for the socket connection + * + * @return InetAddress + */ + public final InetAddress getRemoteAddress() + { + return m_socket != null ? m_socket.getInetAddress() : null; + } + + /** + * Determine if the client name is available + * + * @return boolean + */ + public final boolean hasClientName() + { + return m_clientName != null ? true : false; + } + + /** + * Return the client name + * + * @return + */ + public final String getClientName() + { + return m_clientName; + } + + /** + * Return the count of available bytes in the receive input stream + * + * @return int + * @exception IOException If a network error occurs. + */ + public final int availableBytes() throws IOException + { + if (m_in != null) + return m_in.available(); + return 0; + } + + /** + * Read a packet + * + * @param pkt byte[] + * @param off int + * @param len int + * @return int + * @exception IOException If a network error occurs. + */ + public final int readPacket(byte[] pkt, int off, int len) throws IOException + { + + // Read a packet of data + + if (m_in != null) + return m_in.read(pkt, off, len); + return 0; + } + + /** + * Receive an SMB request packet + * + * @param pkt SMBSrvPacket + * @return int + * @exception IOException If a network error occurs. + */ + public abstract int readPacket(SMBSrvPacket pkt) throws IOException; + + /** + * Send an SMB request packet + * + * @param pkt byte[] + * @param off int + * @param len int + * @exception IOException If a network error occurs. + */ + public final void writePacket(byte[] pkt, int off, int len) throws IOException + { + + // Output the raw packet + + if (m_out != null) + m_out.write(pkt, off, len); + } + + /** + * Send an SMB response packet + * + * @param pkt SMBSrvPacket + * @param len int + * @exception IOException If a network error occurs. + */ + public abstract void writePacket(SMBSrvPacket pkt, int len) throws IOException; + + /** + * Send an SMB response packet + * + * @param pkt SMBSrvPacket + * @exception IOException If a network error occurs. + */ + public final void writePacket(SMBSrvPacket pkt) throws IOException + { + writePacket(pkt, pkt.getLength()); + } + + /** + * Flush the output socket + * + * @exception IOException If a network error occurs + */ + public final void flushPacket() throws IOException + { + if (m_out != null) + m_out.flush(); + } + + /** + * Close the protocol handler + */ + public void closeHandler() + { + + // Close the input stream + + if (m_in != null) + { + try + { + m_in.close(); + } + catch (Exception ex) + { + } + m_in = null; + } + + // Close the output stream + + if (m_out != null) + { + try + { + m_out.close(); + } + catch (Exception ex) + { + } + m_out = null; + } + + // Close the socket + + if (m_socket != null) + { + try + { + m_socket.close(); + } + catch (Exception ex) + { + } + m_socket = null; + } + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/PipeDevice.java b/source/java/org/alfresco/filesys/smb/server/PipeDevice.java new file mode 100644 index 0000000000..946edb7dfa --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/PipeDevice.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.server.core.DeviceInterface; + +/** + * The pipe interface is implemented by classes that provide an interface for a named pipe type + * shared device. + */ +public interface PipeDevice extends DeviceInterface +{ +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/PipeLanmanHandler.java b/source/java/org/alfresco/filesys/smb/server/PipeLanmanHandler.java new file mode 100644 index 0000000000..ee76702d74 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/PipeLanmanHandler.java @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; +import java.util.Enumeration; + +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.core.SharedDeviceList; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.TransactBuffer; +import org.alfresco.filesys.util.DataBuffer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * IPC$ Transaction handler for \PIPE\LANMAN requests. + */ +class PipeLanmanHandler +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Server capability flags + + public static final int WorkStation = 0x00000001; + public static final int Server = 0x00000002; + public static final int SQLServer = 0x00000004; + public static final int DomainCtrl = 0x00000008; + public static final int DomainBakCtrl = 0x00000010; + public static final int TimeSource = 0x00000020; + public static final int AFPServer = 0x00000040; + public static final int NovellServer = 0x00000080; + public static final int DomainMember = 0x00000100; + public static final int PrintServer = 0x00000200; + public static final int DialinServer = 0x00000400; + public static final int UnixServer = 0x00000800; + public static final int NTServer = 0x00001000; + public static final int WfwServer = 0x00002000; + public static final int MFPNServer = 0x00004000; + public static final int NTNonDCServer = 0x00008000; + public static final int PotentialBrowse = 0x00010000; + public static final int BackupBrowser = 0x00020000; + public static final int MasterBrowser = 0x00040000; + public static final int DomainMaster = 0x00080000; + public static final int OSFServer = 0x00100000; + public static final int VMSServer = 0x00200000; + public static final int Win95Plus = 0x00400000; + public static final int DFSRoot = 0x00800000; + public static final int NTCluster = 0x01000000; + public static final int TerminalServer = 0x02000000; + public static final int DCEServer = 0x10000000; + public static final int AlternateXport = 0x20000000; + public static final int LocalListOnly = 0x40000000; + public static final int DomainEnum = 0x80000000; + + /** + * Process a \PIPE\LANMAN transaction request. + * + * @param tbuf Transaction setup, parameter and data buffers + * @param sess SMB server session that received the transaction. + * @param trans Packet to use for reply + * @return true if the transaction has been handled, else false. + * @exception java.io.IOException If an I/O error occurs + * @exception SMBSrvException If an SMB protocol error occurs + */ + public final static boolean processRequest(TransactBuffer tbuf, SMBSrvSession sess, SMBSrvPacket trans) + throws IOException, SMBSrvException + { + + // Create a transaction packet + + SMBSrvTransPacket tpkt = new SMBSrvTransPacket(trans.getBuffer()); + + // Get the transaction command code, parameter descriptor and data descriptor strings from + // the parameter block. + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int cmd = paramBuf.getShort(); + String prmDesc = paramBuf.getString(false); + String dataDesc = paramBuf.getString(false); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("\\PIPE\\LANMAN\\ transact request, cmd=" + cmd + ", prm=" + prmDesc + ", data=" + dataDesc); + + // Call the required transaction handler + + boolean processed = false; + + switch (cmd) + { + + // Share + + case PacketType.RAPShareEnum: + processed = procNetShareEnum(sess, tbuf, prmDesc, dataDesc, tpkt); + break; + + // Get share information + + case PacketType.RAPShareGetInfo: + processed = procNetShareGetInfo(sess, tbuf, prmDesc, dataDesc, tpkt); + break; + + // Workstation information + + case PacketType.RAPWkstaGetInfo: + processed = procNetWkstaGetInfo(sess, tbuf, prmDesc, dataDesc, tpkt); + break; + + // Server information + + case PacketType.RAPServerGetInfo: + processed = procNetServerGetInfo(sess, tbuf, prmDesc, dataDesc, tpkt); + break; + + // Print queue information + + case PacketType.NetPrintQGetInfo: + processed = procNetPrintQGetInfo(sess, tbuf, prmDesc, dataDesc, tpkt); + break; + + // No handler + + default: + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("No handler for \\PIPE\\LANMAN\\ request, cmd=" + cmd + ", prm=" + prmDesc + ", data=" + + dataDesc); + break; + } + + // Return the transaction processed status + + return processed; + } + + /** + * Process a NetServerGetInfo transaction request. + * + * @param sess Server session that received the request. + * @param tbuf Transaction buffer + * @param prmDesc Parameter descriptor string. + * @param dataDesc Data descriptor string. + * @param tpkt Transaction reply packet + * @return true if the transaction has been processed, else false. + */ + protected final static boolean procNetServerGetInfo(SMBSrvSession sess, TransactBuffer tbuf, String prmDesc, + String dataDesc, SMBSrvTransPacket tpkt) throws IOException, SMBSrvException + { + + // Validate the parameter string + + if (prmDesc.compareTo("WrLh") != 0) + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // Unpack the server get information specific parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevel = paramBuf.getShort(); + int bufSize = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetServerGetInfo infoLevel=" + infoLevel); + + // Check if the information level requested and data descriptor string match + + if (infoLevel == 1 && dataDesc.compareTo("B16BBDz") == 0) + { + + // Create the transaction reply data buffer + + TransactBuffer replyBuf = new TransactBuffer(tbuf.isType(), 0, 6, 1024); + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(0); // status code + paramBuf.putShort(0); // converter for strings + paramBuf.putShort(1); // number of entries + + // Pack the data block, calculate the size of the fixed data block + + DataBuffer dataBuf = replyBuf.getDataBuffer(); + int strPos = SMBSrvTransPacket.CalculateDataItemSize("B16BBDz"); + + // Pack the server name pointer and string + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putFixedStringAt(sess.getServerName(), 16, strPos); + + // Pack the major/minor version + + dataBuf.putByte(1); + dataBuf.putByte(0); + + // Pack the server capability flags + + dataBuf.putInt(sess.getSMBServer().getServerType()); + + // Pack the server comment string + + String srvComment = sess.getSMBServer().getComment(); + if (srvComment == null) + srvComment = ""; + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putStringAt(srvComment, strPos, false, true); + + // Set the data block length + + dataBuf.setLength(strPos); + + // Send the transaction response + + tpkt.doTransactionResponse(sess, replyBuf); + } + else + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // We processed the request + + return true; + } + + /** + * Process a NetShareEnum transaction request. + * + * @param sess Server session that received the request. + * @param tbuf Transaction buffer + * @param prmDesc Parameter descriptor string. + * @param dataDesc Data descriptor string. + * @param tpkt Transaction reply packet + * @return true if the transaction has been processed, else false. + */ + protected final static boolean procNetShareEnum(SMBSrvSession sess, TransactBuffer tbuf, String prmDesc, + String dataDesc, SMBSrvTransPacket tpkt) throws IOException, SMBSrvException + { + + // Validate the parameter string + + if (prmDesc.compareTo("WrLeh") != 0) + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // Unpack the server get information specific parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevel = paramBuf.getShort(); + int bufSize = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetShareEnum infoLevel=" + infoLevel); + + // Check if the information level requested and data descriptor string match + + if (infoLevel == 1 && dataDesc.compareTo("B13BWz") == 0) + { + + // Get the share list from the server + + SharedDeviceList shrList = sess.getSMBServer().getShareList(null, sess); + int shrCount = 0; + int strPos = 0; + + if (shrList != null) + { + + // Calculate the fixed data length + + shrCount = shrList.numberOfShares(); + strPos = SMBSrvTransPacket.CalculateDataItemSize("B13BWz") * shrCount; + } + + // Create the transaction reply data buffer + + TransactBuffer replyBuf = new TransactBuffer(tbuf.isType(), 0, 6, bufSize); + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(0); // status code + paramBuf.putShort(0); // converter for strings + paramBuf.putShort(shrCount); // number of entries + paramBuf.putShort(shrCount); // total number of entries + + // Pack the data block + + DataBuffer dataBuf = replyBuf.getDataBuffer(); + Enumeration enm = shrList.enumerateShares(); + + while (enm.hasMoreElements()) + { + + // Get the current share + + SharedDevice shrDev = enm.nextElement(); + + // Pack the share name, share type and comment pointer + + dataBuf.putFixedString(shrDev.getName(), 13); + dataBuf.putByte(0); + dataBuf.putShort(ShareType.asShareInfoType(shrDev.getType())); + dataBuf.putStringPointer(strPos); + + if (shrDev.getComment() != null) + strPos = dataBuf.putStringAt(shrDev.getComment(), strPos, false, true); + else + strPos = dataBuf.putStringAt("", strPos, false, true); + } + + // Set the data block length + + dataBuf.setLength(strPos); + + // Send the transaction response + + tpkt.doTransactionResponse(sess, replyBuf); + } + else + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // We processed the request + + return true; + } + + /** + * Process a NetShareGetInfo transaction request. + * + * @param sess Server session that received the request. + * @param tbuf Transaction buffer + * @param prmDesc Parameter descriptor string. + * @param dataDesc Data descriptor string. + * @param tpkt Transaction reply packet + * @return true if the transaction has been processed, else false. + */ + protected final static boolean procNetShareGetInfo(SMBSrvSession sess, TransactBuffer tbuf, String prmDesc, + String dataDesc, SMBSrvTransPacket tpkt) throws IOException, SMBSrvException + { + + // Validate the parameter string + + if (prmDesc.compareTo("zWrLh") != 0) + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // Unpack the share get information specific parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + String shareName = paramBuf.getString(32, false); + int infoLevel = paramBuf.getShort(); + int bufSize = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetShareGetInfo - " + shareName + ", infoLevel=" + infoLevel); + + // Check if the information level requested and data descriptor string match + + if (infoLevel == 1 && dataDesc.compareTo("B13BWz") == 0) + { + + // Find the required share information + + SharedDevice share = null; + + try + { + + // Get the shared device details + + share = sess.getSMBServer().findShare(null, shareName, ShareType.UNKNOWN, sess, false); + } + catch (Exception ex) + { + } + + if (share == null) + { + sess.sendErrorResponseSMB(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + return true; + } + + // Create the transaction reply data buffer + + TransactBuffer replyBuf = new TransactBuffer(tbuf.isType(), 0, 6, 1024); + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(0); // status code + paramBuf.putShort(0); // converter for strings + paramBuf.putShort(1); // number of entries + + // Pack the data block, calculate the size of the fixed data block + + DataBuffer dataBuf = replyBuf.getDataBuffer(); + int strPos = SMBSrvTransPacket.CalculateDataItemSize("B13BWz"); + + // Pack the share name + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putFixedStringAt(share.getName(), 13, strPos); + + // Pack unknown byte, alignment ? + + dataBuf.putByte(0); + + // Pack the share type flags + + dataBuf.putShort(share.getType()); + + // Pack the share comment + + dataBuf.putStringPointer(strPos); + + if (share.getComment() != null) + strPos = dataBuf.putStringAt(share.getComment(), strPos, false, true); + else + strPos = dataBuf.putStringAt("", strPos, false, true); + + // Set the data block length + + dataBuf.setLength(strPos); + + // Send the transaction response + + tpkt.doTransactionResponse(sess, replyBuf); + } + else + { + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetShareGetInfo - UNSUPPORTED " + shareName + ", infoLevel=" + infoLevel + ", dataDesc=" + + dataDesc); + + // Server error + + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + } + + // We processed the request + + return true; + } + + /** + * Process a NetWkstaGetInfo transaction request. + * + * @param sess Server session that received the request. + * @param tbuf Transaction buffer + * @param prmDesc Parameter descriptor string. + * @param dataDesc Data descriptor string. + * @param tpkt Transaction reply packet + * @return true if the transaction has been processed, else false. + */ + protected final static boolean procNetWkstaGetInfo(SMBSrvSession sess, TransactBuffer tbuf, String prmDesc, + String dataDesc, SMBSrvTransPacket tpkt) throws IOException, SMBSrvException + { + + // Validate the parameter string + + if (prmDesc.compareTo("WrLh") != 0) + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // Unpack the share get information specific parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + int infoLevel = paramBuf.getShort(); + int bufSize = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetWkstaGetInfo infoLevel=" + infoLevel); + + // Check if the information level requested and data descriptor string match + + if ((infoLevel == 1 && dataDesc.compareTo("zzzBBzzz") == 0) + || (infoLevel == 10 && dataDesc.compareTo("zzzBBzz") == 0)) + { + + // Create the transaction reply data buffer + + TransactBuffer replyBuf = new TransactBuffer(tbuf.isType(), 0, 6, 1024); + + // Pack the data block, calculate the size of the fixed data block + + DataBuffer dataBuf = replyBuf.getDataBuffer(); + int strPos = SMBSrvTransPacket.CalculateDataItemSize(dataDesc); + + // Pack the server name + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putStringAt(sess.getServerName(), strPos, false, true); + + // Pack the user name + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putStringAt("", strPos, false, true); + + // Pack the domain name + + dataBuf.putStringPointer(strPos); + + String domain = sess.getServer().getConfiguration().getDomainName(); + if (domain == null) + domain = ""; + strPos = dataBuf.putStringAt(domain, strPos, false, true); + + // Pack the major/minor version number + + dataBuf.putByte(4); + dataBuf.putByte(2); + + // Pack the logon domain + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putStringAt("", strPos, false, true); + + // Check if the other domains should be packed + + if (infoLevel == 1 && dataDesc.compareTo("zzzBBzzz") == 0) + { + + // Pack the other domains + + dataBuf.putStringPointer(strPos); + strPos = dataBuf.putStringAt("", strPos, false, true); + } + + // Set the data block length + + dataBuf.setLength(strPos); + + // Pack the parameter block + + paramBuf = replyBuf.getParameterBuffer(); + + paramBuf.putShort(0); // status code + paramBuf.putShort(0); // converter for strings + paramBuf.putShort(dataBuf.getLength()); + paramBuf.putShort(0); // number of entries + + // Send the transaction response + + tpkt.doTransactionResponse(sess, replyBuf); + } + else + { + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetWkstaGetInfo UNSUPPORTED infoLevel=" + infoLevel + ", dataDesc=" + dataDesc); + + // Unsupported request + + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + } + + // We processed the request + + return true; + } + + /** + * Process a NetPrintQGetInfo transaction request. + * + * @param sess Server session that received the request. + * @param tbuf Transaction buffer + * @param prmDesc Parameter descriptor string. + * @param dataDesc Data descriptor string. + * @param tpkt Transaction reply packet + * @return true if the transaction has been processed, else false. + */ + protected final static boolean procNetPrintQGetInfo(SMBSrvSession sess, TransactBuffer tbuf, String prmDesc, + String dataDesc, SMBSrvTransPacket tpkt) throws IOException, SMBSrvException + { + + // Validate the parameter string + + if (prmDesc.compareTo("zWrLh") != 0) + throw new SMBSrvException(SMBStatus.SRVInternalServerError, SMBStatus.ErrSrv); + + // Unpack the share get information specific parameters + + DataBuffer paramBuf = tbuf.getParameterBuffer(); + + String shareName = paramBuf.getString(32, false); + int infoLevel = paramBuf.getShort(); + int bufSize = paramBuf.getShort(); + + // Debug + + if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC)) + logger.debug("NetPrintQGetInfo - " + shareName + ", infoLevel=" + infoLevel); + + // We did not process the request + + return false; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/ProtocolFactory.java b/source/java/org/alfresco/filesys/smb/server/ProtocolFactory.java new file mode 100644 index 0000000000..0ae7640ce2 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/ProtocolFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.smb.Dialect; + +/** + * SMB Protocol Factory Class. + *

    + * The protocol factory class generates protocol handlers for SMB dialects. + */ +class ProtocolFactory +{ + + /** + * ProtocolFactory constructor comment. + */ + public ProtocolFactory() + { + super(); + } + + /** + * Return a protocol handler for the specified SMB dialect type, or null if there is no + * appropriate protocol handler. + * + * @param dialect int + * @return ProtocolHandler + */ + protected static ProtocolHandler getHandler(int dialect) + { + + // Determine the SMB dialect type + + ProtocolHandler handler = null; + + switch (dialect) + { + + // Core dialect + + case Dialect.Core: + case Dialect.CorePlus: + handler = new CoreProtocolHandler(); + break; + + // LanMan dialect + + case Dialect.DOSLanMan1: + case Dialect.DOSLanMan2: + case Dialect.LanMan1: + case Dialect.LanMan2: + case Dialect.LanMan2_1: + handler = new LanManProtocolHandler(); + break; + + // NT dialect + + case Dialect.NT: + handler = new NTProtocolHandler(); + break; + } + + // Return the protocol handler + + return handler; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/ProtocolHandler.java b/source/java/org/alfresco/filesys/smb/server/ProtocolHandler.java new file mode 100644 index 0000000000..f5229ebc09 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/ProtocolHandler.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; + +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.DiskSizeInterface; +import org.alfresco.filesys.server.filesys.DiskVolumeInterface; +import org.alfresco.filesys.server.filesys.SrvDiskInfo; +import org.alfresco.filesys.server.filesys.TooManyConnectionsException; +import org.alfresco.filesys.server.filesys.VolumeInfo; +import org.alfresco.filesys.smb.PacketType; + +/** + * Protocol handler abstract base class. + *

    + * The protocol handler class is the base of all SMB protocol/dialect handler classes. + */ +abstract class ProtocolHandler +{ + + // Server session that this protocol handler is associated with. + + protected SMBSrvSession m_sess; + + /** + * Create a protocol handler for the specified session. + */ + protected ProtocolHandler() + { + } + + /** + * Create a protocol handler for the specified session. + * + * @param sess SMBSrvSession + */ + protected ProtocolHandler(SMBSrvSession sess) + { + m_sess = sess; + } + + /** + * Return the protocol handler name. + * + * @return java.lang.String + */ + public abstract String getName(); + + /** + * Run the SMB protocol handler for this server session. + * + * @exception java.io.IOException + * @exception SMBSrvException + */ + public abstract boolean runProtocol() throws IOException, SMBSrvException, TooManyConnectionsException; + + /** + * Get the server session that this protocol handler is associated with. + * + * @param sess SMBSrvSession + */ + protected final SMBSrvSession getSession() + { + return m_sess; + } + + /** + * Set the server session that this protocol handler is associated with. + * + * @param sess SMBSrvSession + */ + protected final void setSession(SMBSrvSession sess) + { + m_sess = sess; + } + + /** + * Determine if the request is a chained (AndX) type command and there is a chained command in + * this request. + * + * @param pkt SMBSrvPacket + * @return true if there is a chained request to be handled, else false. + */ + protected final boolean hasChainedCommand(SMBSrvPacket pkt) + { + + // Determine if the command code is an AndX command + + int cmd = pkt.getCommand(); + + if (cmd == PacketType.SessionSetupAndX || cmd == PacketType.TreeConnectAndX || cmd == PacketType.OpenAndX + || cmd == PacketType.WriteAndX || cmd == PacketType.ReadAndX || cmd == PacketType.LogoffAndX + || cmd == PacketType.LockingAndX || cmd == PacketType.NTCreateAndX) + { + + // Check if there is a chained command + + return pkt.hasAndXCommand(); + } + + // Not a chained type command + + return false; + } + + /** + * Get disk sizing information from the specified driver and context. + * + * @param disk DiskInterface + * @param ctx DiskDeviceContext + * @return SrvDiskInfo + * @exception IOException + */ + protected final SrvDiskInfo getDiskInformation(DiskInterface disk, DiskDeviceContext ctx) throws IOException + { + + // Get the static disk information from the context, if available + + SrvDiskInfo diskInfo = ctx.getDiskInformation(); + + // If we did not get valid disk information from the device context check if the driver + // implements the + // disk sizing interface + + if (diskInfo == null) + diskInfo = new SrvDiskInfo(); + + // Check if the driver implements the dynamic sizing interface to get realtime disk size + // information + + if (disk instanceof DiskSizeInterface) + { + + // Get the dynamic disk sizing information + + DiskSizeInterface sizeInterface = (DiskSizeInterface) disk; + sizeInterface.getDiskInformation(ctx, diskInfo); + } + + // Return the disk information + + return diskInfo; + } + + /** + * Get disk volume information from the specified driver and context + * + * @param disk DiskInterface + * @param ctx DiskDeviceContext + * @return VolumeInfo + */ + protected final VolumeInfo getVolumeInformation(DiskInterface disk, DiskDeviceContext ctx) + { + + // Get the static volume information from the context, if available + + VolumeInfo volInfo = ctx.getVolumeInformation(); + + // If we did not get valid volume information from the device context check if the driver + // implements the + // disk volume interface + + if (disk instanceof DiskVolumeInterface) + { + + // Get the dynamic disk volume information + + DiskVolumeInterface volInterface = (DiskVolumeInterface) disk; + volInfo = volInterface.getVolumeInformation(ctx); + } + + // If we still have not got valid volume information then create empty volume information + + if (volInfo == null) + volInfo = new VolumeInfo(""); + + // Return the volume information + + return volInfo; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/QueryInfoPacker.java b/source/java/org/alfresco/filesys/smb/server/QueryInfoPacker.java new file mode 100644 index 0000000000..bc1f585c63 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/QueryInfoPacker.java @@ -0,0 +1,743 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.UnsupportedInfoLevelException; +import org.alfresco.filesys.smb.FileInfoLevel; +import org.alfresco.filesys.smb.NTTime; +import org.alfresco.filesys.smb.SMBDate; +import org.alfresco.filesys.smb.WinNT; +import org.alfresco.filesys.smb.server.ntfs.StreamInfo; +import org.alfresco.filesys.smb.server.ntfs.StreamInfoList; +import org.alfresco.filesys.util.DataBuffer; + +/** + * Query File Information Packer Class + *

    + * Packs file/directory information for the specified information level. + */ +public class QueryInfoPacker +{ + + /** + * Pack a file information object into the specified buffer, using the specified information + * level. + * + * @param info File information to be packed. + * @param buf Buffer to pack the data into. + * @param infoLevel File information level. + * @param uni Pack Unicode strings if true, else pack ASCII strings + * @return int Length of data packed + */ + public final static int packInfo(FileInfo info, DataBuffer buf, int infoLevel, boolean uni) + throws UnsupportedInfoLevelException + { + + // Determine the information level + + int curPos = buf.getPosition(); + + switch (infoLevel) + { + + // Standard information + + case FileInfoLevel.PathStandard: + packInfoStandard(info, buf, false, uni); + break; + + // Standard information plus EA size + + case FileInfoLevel.PathQueryEASize: + packInfoStandard(info, buf, true, uni); + break; + + // Extended attributes list + + case FileInfoLevel.PathQueryEAsFromList: + break; + + // All extended attributes + + case FileInfoLevel.PathAllEAs: + break; + + // Validate a file name + + case FileInfoLevel.PathIsNameValid: + break; + + // Basic file information + + case FileInfoLevel.PathFileBasicInfo: + case FileInfoLevel.NTFileBasicInfo: + packBasicFileInfo(info, buf); + break; + + // Standard file information + + case FileInfoLevel.PathFileStandardInfo: + case FileInfoLevel.NTFileStandardInfo: + packStandardFileInfo(info, buf); + break; + + // Extended attribute information + + case FileInfoLevel.PathFileEAInfo: + case FileInfoLevel.NTFileEAInfo: + packEAFileInfo(info, buf); + break; + + // File name information + + case FileInfoLevel.PathFileNameInfo: + case FileInfoLevel.NTFileNameInfo: + packNameFileInfo(info, buf, uni); + break; + + // All information + + case FileInfoLevel.PathFileAllInfo: + case FileInfoLevel.NTFileAllInfo: + packAllFileInfo(info, buf, uni); + break; + + // Alternate name information + + case FileInfoLevel.PathFileAltNameInfo: + case FileInfoLevel.NTFileAltNameInfo: + packAlternateNameFileInfo(info, buf); + break; + + // Stream information + + case FileInfoLevel.PathFileStreamInfo: + case FileInfoLevel.NTFileStreamInfo: + packStreamFileInfo(info, buf, uni); + break; + + // Compression information + + case FileInfoLevel.PathFileCompressionInfo: + case FileInfoLevel.NTFileCompressionInfo: + packCompressionFileInfo(info, buf); + break; + + // File internal information + + case FileInfoLevel.NTFileInternalInfo: + packFileInternalInfo(info, buf); + break; + + // File position information + + case FileInfoLevel.NTFilePositionInfo: + packFilePositionInfo(info, buf); + break; + + // Attribute tag information + + case FileInfoLevel.NTAttributeTagInfo: + packFileAttributeTagInfo(info, buf); + break; + + // Network open information + + case FileInfoLevel.NTNetworkOpenInfo: + packFileNetworkOpenInfo(info, buf); + break; + } + + // Return the length of the data that was packed + + return buf.getPosition() - curPos; + } + + /** + * Pack the standard file information + * + * @param info File information + * @param buf Buffer to pack data into + * @param eaFlag Return EA size + * @param uni Pack unicode strings + */ + private static void packInfoStandard(FileInfo info, DataBuffer buf, boolean eaFlag, boolean uni) + { + + // Information format :- + // SMB_DATE CreationDate + // SMB_TIME CreationTime + // SMB_DATE LastAccessDate + // SMB_TIME LastAccessTime + // SMB_DATE LastWriteDate + // SMB_TIME LastWriteTime + // ULONG File size + // ULONG Allocation size + // USHORT File attributes + // [ ULONG EA size ] + + // Pack the creation date/time + + SMBDate dateTime = new SMBDate(0); + + if (info.hasCreationDateTime()) + { + dateTime.setTime(info.getCreationDateTime()); + buf.putShort(dateTime.asSMBDate()); + buf.putShort(dateTime.asSMBTime()); + } + else + buf.putZeros(4); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + dateTime.setTime(info.getAccessDateTime()); + buf.putShort(dateTime.asSMBDate()); + buf.putShort(dateTime.asSMBTime()); + } + else + buf.putZeros(4); + + // Pack the last write date/time + + if (info.hasModifyDateTime()) + { + dateTime.setTime(info.getModifyDateTime()); + buf.putShort(dateTime.asSMBDate()); + buf.putShort(dateTime.asSMBTime()); + } + else + buf.putZeros(4); + + // Pack the file size and allocation size + + buf.putInt(info.getSizeInt()); + + if (info.getAllocationSize() < info.getSize()) + buf.putInt(info.getSizeInt()); + else + buf.putInt(info.getAllocationSizeInt()); + + // Pack the file attributes + + buf.putShort(info.getFileAttributes()); + + // Pack the EA size, always zero + + if (eaFlag == true) + buf.putZeros(4); + } + + /** + * Pack the basic file information (level 0x101) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packBasicFileInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // LARGE_INTEGER Creation date/time + // LARGE_INTEGER Access date/time + // LARGE_INTEGER Write date/time + // LARGE_INTEGER Change date/time + // UINT Attributes + // UINT Unknown + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getAccessDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write and change date/time + + if (info.hasModifyDateTime()) + { + long ntTime = NTTime.toNTTime(info.getModifyDateTime()); + buf.putLong(ntTime); + buf.putLong(ntTime); + } + else + buf.putZeros(16); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Pack unknown value + + buf.putZeros(4); + } + + /** + * Pack the standard file information (level 0x102) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packStandardFileInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // LARGE_INTEGER AllocationSize + // LARGE_INTEGER EndOfFile + // UINT NumberOfLinks + // BOOLEAN DeletePending + // BOOLEAN Directory + // SHORT Unknown + + // Pack the allocation and file sizes + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + buf.putLong(info.getSize()); + + // Pack the number of links, always one for now + + buf.putInt(1); + + // Pack the delete pending and directory flags + + buf.putByte(0); + buf.putByte(info.isDirectory() ? 1 : 0); + + // buf.putZeros(2); + } + + /** + * Pack the extended attribute information (level 0x103) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packEAFileInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG EASize + + // Pack the extended attribute size + + buf.putInt(0); + } + + /** + * Pack the file name information (level 0x104) + * + * @param info File information + * @param buf Buffer to pack data into + * @param uni Pack unicode strings + */ + private static void packNameFileInfo(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // UINT FileNameLength + // WCHAR FileName[] + + // Pack the file name length and name string as Unicode + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + buf.putString(info.getFileName(), uni, false); + } + + /** + * Pack the all file information (level 0x107) + * + * @param info File information + * @param buf Buffer to pack data into + * @param uni Pack unicode strings + */ + private static void packAllFileInfo(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // LARGE_INTEGER Creation date/time + // LARGE_INTEGER Access date/time + // LARGE_INTEGER Write date/time + // LARGE_INTEGER Change date/time + // UINT Attributes + // UINT Number of links + // LARGE_INTEGER Allocation + // LARGE_INTEGER Size + // BYTE Delete pending + // BYTE Directory flag + // 2 byte longword alignment + // UINT EA Size + // UINT Access mask + // UINT File name length + // WCHAR FileName[] + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getAccessDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write and change date/time + + if (info.hasModifyDateTime()) + { + long ntTime = NTTime.toNTTime(info.getModifyDateTime()); + buf.putLong(ntTime); + buf.putLong(ntTime); + } + else + buf.putZeros(16); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Number of links + + buf.putInt(1); + + // Pack the allocation and used file sizes + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + buf.putLong(info.getSize()); + + // Pack the delete pending and directory flags + + buf.putByte(0); + buf.putByte(info.isDirectory() ? 1 : 0); + buf.putShort(0); // Alignment + + // EA list size + + buf.putInt(0); + + // Access mask + + buf.putInt(0x00000003); + + // File name length in bytes and file name, Unicode + + int nameLen = info.getFileName().length(); + if (uni) + nameLen *= 2; + + buf.putInt(nameLen); + buf.putString(info.getFileName(), uni, false); + } + + /** + * Pack the alternate name information (level 0x108) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packAlternateNameFileInfo(FileInfo info, DataBuffer buf) + { + } + + /** + * Pack the stream information (level 0x109) + * + * @param info File information + * @param buf Buffer to pack data into + * @param uni Pack unicode strings + */ + private static void packStreamFileInfo(FileInfo info, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG OffsetToNextStreamInfo + // ULONG NameLength (in bytes) + // LARGE_INTEGER StreamSize + // LARGE_INTEGER StreamAlloc + // WCHAR StreamName[] + + // Pack a dummy data stream for now + + String streamName = "::$DATA"; + + buf.putInt(0); // offset to next info (no more info) + + int nameLen = streamName.length(); + if (uni) + nameLen *= 2; + buf.putInt(nameLen); + + // Stream size + + buf.putLong(info.getSize()); + + // Allocation size + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + buf.putString(streamName, uni, false); + } + + /** + * Pack the stream information (level 0x109) + * + * @param streams List of streams + * @param buf Buffer to pack data into + * @param uni Pack unicode strings + * @return int + */ + public static int packStreamFileInfo(StreamInfoList streams, DataBuffer buf, boolean uni) + { + + // Information format :- + // ULONG OffsetToNextStreamInfo + // ULONG NameLength (in bytes) + // LARGE_INTEGER StreamSize + // LARGE_INTEGER StreamAlloc + // WCHAR StreamName[] + + // Loop through the available streams + + int curPos = buf.getPosition(); + int startPos = curPos; + int pos = 0; + + for (int i = 0; i < streams.numberOfStreams(); i++) + { + + // Get the current stream information + + StreamInfo sinfo = streams.getStreamAt(i); + + // Skip the offset to the next stream information structure + + buf.putInt(0); + + // Set the stream name length + + int nameLen = sinfo.getName().length(); + if (uni) + nameLen *= 2; + buf.putInt(nameLen); + + // Stream size + + buf.putLong(sinfo.getSize()); + + // Allocation size + + if (sinfo.getAllocationSize() < sinfo.getSize()) + buf.putLong(sinfo.getSize()); + else + buf.putLong(sinfo.getAllocationSize()); + + buf.putString(sinfo.getName(), uni, false); + + // Word align the buffer + + buf.wordAlign(); + + // Fill in the offset to the next stream information, if this is not the last stream + + if (i < (streams.numberOfStreams() - 1)) + { + + // Fill in the offset from the current stream information structure to the next + + pos = buf.getPosition(); + buf.setPosition(startPos); + buf.putInt(pos - startPos); + buf.setPosition(pos); + startPos = pos; + } + } + + // Return the data length + + return buf.getPosition() - curPos; + } + + /** + * Pack the compression information (level 0x10B) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packCompressionFileInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // LARGE_INTEGER CompressedSize + // ULONG CompressionFormat (sess WinNT class) + + buf.putLong(info.getSize()); + buf.putInt(WinNT.CompressionFormatNone); + } + + /** + * Pack the file internal information (level 1006) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packFileInternalInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG Unknown1 + // ULONG Unknown2 + + buf.putInt(1); + buf.putInt(0); + } + + /** + * Pack the file position information (level 1014) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packFilePositionInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG Unknown1 + // ULONG Unknown2 + + buf.putInt(0); + buf.putInt(0); + } + + /** + * Pack the network open information (level 1034) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packFileNetworkOpenInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // LARGE_INTEGER Creation date/time + // LARGE_INTEGER Access date/time + // LARGE_INTEGER Write date/time + // LARGE_INTEGER Change date/time + // LARGE_INTEGER Allocation + // LARGE_INTEGER Size + // UINT Attributes + // UNIT Unknown + + // Pack the creation date/time + + if (info.hasCreationDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getCreationDateTime())); + } + else + buf.putZeros(8); + + // Pack the last access date/time + + if (info.hasAccessDateTime()) + { + buf.putLong(NTTime.toNTTime(info.getAccessDateTime())); + } + else + buf.putZeros(8); + + // Pack the last write and change date/time + + if (info.hasModifyDateTime()) + { + long ntTime = NTTime.toNTTime(info.getModifyDateTime()); + buf.putLong(ntTime); + buf.putLong(ntTime); + } + else + buf.putZeros(16); + + // Pack the allocation and used file sizes + + if (info.getAllocationSize() < info.getSize()) + buf.putLong(info.getSize()); + else + buf.putLong(info.getAllocationSize()); + + buf.putLong(info.getSize()); + + // Pack the file attributes + + buf.putInt(info.getFileAttributes()); + + // Pack the unknown value + + buf.putInt(0); + } + + /** + * Pack the attribute tag information (level 1035) + * + * @param info File information + * @param buf Buffer to pack data into + */ + private static void packFileAttributeTagInfo(FileInfo info, DataBuffer buf) + { + + // Information format :- + // ULONG Unknown1 + // ULONG Unknown2 + + buf.putLong(0); + buf.putLong(0); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/SMBPacket.java b/source/java/org/alfresco/filesys/smb/server/SMBPacket.java new file mode 100644 index 0000000000..f56cffb6ec --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBPacket.java @@ -0,0 +1,1035 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.netbios.NetBIOSSession; +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.util.DataPacker; + +/** + * SMB packet type class + */ +public class SMBPacket +{ + + // SMB packet offsets, assuming an RFC NetBIOS transport + + public static final int SIGNATURE = RFCNetBIOSProtocol.HEADER_LEN; + public static final int COMMAND = 4 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ERRORCODE = 5 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ERRORCLASS = 5 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ERROR = 7 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int FLAGS = 9 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int FLAGS2 = 10 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int PIDHIGH = 12 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int SID = 18 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int SEQNO = 20 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int TID = 24 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int PID = 26 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int UID = 28 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int MID = 30 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int WORDCNT = 32 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ANDXCOMMAND = 33 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ANDXRESERVED = 34 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int PARAMWORDS = 33 + RFCNetBIOSProtocol.HEADER_LEN; + + // SMB packet header length for a transaction type request + + public static final int TRANS_HEADERLEN = 66 + RFCNetBIOSProtocol.HEADER_LEN; + + // Minimum receive length for a valid SMB packet + + public static final int MIN_RXLEN = 32; + + // Default buffer size to allocate for SMB packets + + public static final int DEFAULT_BUFSIZE = 4096; + + // Flag bits + + public static final int FLG_SUBDIALECT = 0x01; + public static final int FLG_CASELESS = 0x08; + public static final int FLG_CANONICAL = 0x10; + public static final int FLG_OPLOCK = 0x20; + public static final int FLG_NOTIFY = 0x40; + public static final int FLG_RESPONSE = 0x80; + + // Flag2 bits + + public static final int FLG2_LONGFILENAMES = 0x0001; + public static final int FLG2_EXTENDEDATTRIB = 0x0002; + public static final int FLG2_SECURITYSIGS = 0x0004; + public static final int FLG2_LONGNAMESUSED = 0x0040; + public static final int FLG2_EXTENDNEGOTIATE = 0x0800; + public static final int FLG2_DFSRESOLVE = 0x1000; + public static final int FLG2_READIFEXE = 0x2000; + public static final int FLG2_LONGERRORCODE = 0x4000; + public static final int FLG2_UNICODE = 0x8000; + + // Security mode bits + + public static final int SEC_USER = 0x0001; + public static final int SEC_ENCRYPT = 0x0002; + + // Raw mode bits + + public static final int RAW_READ = 0x0001; + public static final int RAW_WRITE = 0x0002; + + // SMB packet buffer + + private byte[] m_smbbuf; + + // Packet type + + private int m_pkttype; + + // Current byte area pack/unpack position + + protected int m_pos; + protected int m_endpos; + + /** + * Default constructor + */ + public SMBPacket() + { + m_smbbuf = new byte[DEFAULT_BUFSIZE]; + InitializeBuffer(); + } + + /** + * Construct an SMB packet using the specified packet buffer. + * + * @param buf SMB packet buffer. + */ + public SMBPacket(byte[] buf) + { + m_smbbuf = buf; + } + + /** + * Construct an SMB packet of the specified size. + * + * @param siz Size of SMB packet buffer to allocate. + */ + public SMBPacket(int siz) + { + m_smbbuf = new byte[siz]; + InitializeBuffer(); + } + + /** + * Copy constructor + * + * @param pkt SMBPacket + */ + public SMBPacket(SMBPacket pkt) + { + + // Allocate a new buffer + + m_smbbuf = new byte[pkt.getBuffer().length]; + + // Copy the valid data to the new packet + + System.arraycopy(pkt.getBuffer(), 0, m_smbbuf, 0, pkt.getLength()); + } + + /** + * Clear the data byte count + */ + public final void clearBytes() + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(0, m_smbbuf, offset); + } + + /** + * Dump the SMB packet to the debug stream + */ + public final void DumpPacket() + { + } + + /** + * Check if the error class/code match the specified error/class + * + * @param errClass int + * @param errCode int + * @return boolean + */ + public final boolean equalsError(int errClass, int errCode) + { + if (getErrorClass() == errClass && getErrorCode() == errCode) + return true; + return false; + } + + /** + * Get the secondary command code + * + * @return Secondary command code + */ + public final int getAndXCommand() + { + return (int) (m_smbbuf[ANDXCOMMAND] & 0xFF); + } + + /** + * Return the byte array used for the SMB packet + * + * @return Byte array used for the SMB packet. + */ + public final byte[] getBuffer() + { + return m_smbbuf; + } + + /** + * Return the total buffer size available to the SMB request + * + * @return Total SMB buffer length available. + */ + public final int getBufferLength() + { + return m_smbbuf.length - RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Get the data byte count for the SMB packet + * + * @return Data byte count + */ + public final int getByteCount() + { + + // Calculate the offset of the byte count + + int pos = PARAMWORDS + (2 * getParameterCount()); + return (int) DataPacker.getIntelShort(m_smbbuf, pos); + } + + /** + * Get the data byte area offset within the SMB packet + * + * @return Data byte offset within the SMB packet. + */ + public final int getByteOffset() + { + + // Calculate the offset of the byte buffer + + int pCnt = getParameterCount(); + int pos = WORDCNT + (2 * pCnt) + 3; + return pos; + } + + /** + * Get the SMB command + * + * @return SMB command code. + */ + public final int getCommand() + { + return (int) (m_smbbuf[COMMAND] & 0xFF); + } + + /** + * Determine if normal or long error codes have been returned + * + * @return boolean + */ + public final boolean hasLongErrorCode() + { + if ((getFlags2() & FLG2_LONGERRORCODE) == 0) + return false; + return true; + } + + /** + * Check if the packet contains ASCII or Unicode strings + * + * @return boolean + */ + public final boolean isUnicode() + { + return (getFlags2() & FLG2_UNICODE) != 0 ? true : false; + } + + /** + * Check if the packet is using caseless filenames + * + * @return boolean + */ + public final boolean isCaseless() + { + return (getFlags() & FLG_CASELESS) != 0 ? true : false; + } + + /** + * Check if long file names are being used + * + * @return boolean + */ + public final boolean isLongFileNames() + { + return (getFlags2() & FLG2_LONGFILENAMES) != 0 ? true : false; + } + + /** + * Check if long error codes are being used + * + * @return boolean + */ + public final boolean isLongErrorCode() + { + return (getFlags2() & FLG2_LONGERRORCODE) != 0 ? true : false; + } + + /** + * Get the SMB error class + * + * @return SMB error class. + */ + public final int getErrorClass() + { + return (int) m_smbbuf[ERRORCLASS] & 0xFF; + } + + /** + * Get the SMB error code + * + * @return SMB error code. + */ + public final int getErrorCode() + { + return (int) m_smbbuf[ERROR] & 0xFF; + } + + /** + * Get the SMB flags value. + * + * @return SMB flags value. + */ + public final int getFlags() + { + return (int) m_smbbuf[FLAGS] & 0xFF; + } + + /** + * Get the SMB flags2 value. + * + * @return SMB flags2 value. + */ + public final int getFlags2() + { + return (int) DataPacker.getIntelShort(m_smbbuf, FLAGS2); + } + + /** + * Calculate the total used packet length. + * + * @return Total used packet length. + */ + public final int getLength() + { + return (getByteOffset() + getByteCount()) - SIGNATURE; + } + + /** + * Get the long SMB error code + * + * @return Long SMB error code. + */ + public final int getLongErrorCode() + { + return DataPacker.getIntelInt(m_smbbuf, ERRORCODE); + } + + /** + * Get the multiplex identifier. + * + * @return Multiplex identifier. + */ + public final int getMultiplexId() + { + return DataPacker.getIntelShort(m_smbbuf, MID); + } + + /** + * Get a parameter word from the SMB packet. + * + * @param idx Parameter index (zero based). + * @return Parameter word value. + * @exception java.lang.IndexOutOfBoundsException If the parameter index is out of range. + */ + public final int getParameter(int idx) throws java.lang.IndexOutOfBoundsException + { + + // Range check the parameter index + + if (idx > getParameterCount()) + throw new java.lang.IndexOutOfBoundsException(); + + // Calculate the parameter word offset + + int pos = WORDCNT + (2 * idx) + 1; + return (int) (DataPacker.getIntelShort(m_smbbuf, pos) & 0xFFFF); + } + + /** + * Get the specified parameter words, as an int value. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + public final int getParameterLong(int idx) + { + int pos = WORDCNT + (2 * idx) + 1; + return DataPacker.getIntelInt(m_smbbuf, pos); + } + + /** + * Get the parameter count + * + * @return Parameter word count. + */ + public final int getParameterCount() + { + return (int) m_smbbuf[WORDCNT]; + } + + /** + * Get the process indentifier (PID) + * + * @return Process identifier value. + */ + public final int getProcessId() + { + return DataPacker.getIntelShort(m_smbbuf, PID); + } + + /** + * Get the tree identifier (TID) + * + * @return Tree identifier (TID) + */ + public final int getTreeId() + { + return DataPacker.getIntelShort(m_smbbuf, TID); + } + + /** + * Get the user identifier (UID) + * + * @return User identifier (UID) + */ + public final int getUserId() + { + return DataPacker.getIntelShort(m_smbbuf, UID); + } + + /** + * Initialize the SMB packet buffer. + */ + private final void InitializeBuffer() + { + + // Set the packet signature + + m_smbbuf[SIGNATURE] = (byte) 0xFF; + m_smbbuf[SIGNATURE + 1] = (byte) 'S'; + m_smbbuf[SIGNATURE + 2] = (byte) 'M'; + m_smbbuf[SIGNATURE + 3] = (byte) 'B'; + } + + /** + * Determine if this packet is an SMB response, or command packet + * + * @return true if this SMB packet is a response, else false + */ + public final boolean isResponse() + { + int resp = getFlags(); + if ((resp & FLG_RESPONSE) != 0) + return true; + return false; + } + + /** + * Check if the response packet is valid, ie. type and flags + * + * @return true if the SMB packet is a response packet and the response is valid, else false. + */ + public final boolean isValidResponse() + { + + // Check if this is a response packet, and the correct type of packet + + if (isResponse() && getCommand() == m_pkttype) + { + + // Check if standard error codes or NT 32-bit error codes are being used + + if ((getFlags2() & FLG2_LONGERRORCODE) == 0) + { + if (getErrorClass() == SMBStatus.Success) + return true; + } + else if (getLongErrorCode() == SMBStatus.NTSuccess) + return true; + } + return false; + } + + /** + * Pack a byte (8 bit) value into the byte area + * + * @param val byte + */ + public final void packByte(byte val) + { + m_smbbuf[m_pos++] = val; + } + + /** + * Pack a byte (8 bit) value into the byte area + * + * @param val int + */ + public final void packByte(int val) + { + m_smbbuf[m_pos++] = (byte) val; + } + + /** + * Pack the specified bytes into the byte area + * + * @param byts byte[] + * @param len int + */ + public final void packBytes(byte[] byts, int len) + { + for (int i = 0; i < len; i++) + m_smbbuf[m_pos++] = byts[i]; + } + + /** + * Pack a string using either ASCII or Unicode into the byte area + * + * @param str String + * @param uni boolean + */ + public final void packString(String str, boolean uni) + { + + // Check for Unicode or ASCII + + if (uni) + { + + // Word align the buffer position, pack the Unicode string + + m_pos = DataPacker.wordAlign(m_pos); + DataPacker.putUnicodeString(str, m_smbbuf, m_pos, true); + m_pos += (str.length() * 2) + 2; + } + else + { + + // Pack the ASCII string + + DataPacker.putString(str, m_smbbuf, m_pos, true); + m_pos += str.length() + 1; + } + } + + /** + * Pack a word (16 bit) value into the byte area + * + * @param val int + */ + public final void packWord(int val) + { + DataPacker.putIntelShort(val, m_smbbuf, m_pos); + m_pos += 2; + } + + /** + * Pack a 32 bit integer value into the byte area + * + * @param val int + */ + public final void packInt(int val) + { + DataPacker.putIntelInt(val, m_smbbuf, m_pos); + m_pos += 4; + } + + /** + * Pack a long integer (64 bit) value into the byte area + * + * @param val long + */ + public final void packLong(long val) + { + DataPacker.putIntelLong(val, m_smbbuf, m_pos); + m_pos += 8; + } + + /** + * Return the current byte area buffer position + * + * @return int + */ + public final int getPosition() + { + return m_pos; + } + + /** + * Unpack a byte value from the byte area + * + * @return int + */ + public final int unpackByte() + { + return (int) m_smbbuf[m_pos++]; + } + + /** + * Unpack a block of bytes from the byte area + * + * @param len int + * @return byte[] + */ + public final byte[] unpackBytes(int len) + { + if (len <= 0) + return null; + + byte[] buf = new byte[len]; + System.arraycopy(m_smbbuf, m_pos, buf, 0, len); + m_pos += len; + return buf; + } + + /** + * Unpack a word (16 bit) value from the byte area + * + * @return int + */ + public final int unpackWord() + { + int val = DataPacker.getIntelShort(m_smbbuf, m_pos); + m_pos += 2; + return val; + } + + /** + * Unpack an integer (32 bit) value from the byte/parameter area + * + * @return int + */ + public final int unpackInt() + { + int val = DataPacker.getIntelInt(m_smbbuf, m_pos); + m_pos += 4; + return val; + } + + /** + * Unpack a long integer (64 bit) value from the byte area + * + * @return long + */ + public final long unpackLong() + { + long val = DataPacker.getIntelLong(m_smbbuf, m_pos); + m_pos += 8; + return val; + } + + /** + * Unpack a string from the byte area + * + * @param uni boolean + * @return String + */ + public final String unpackString(boolean uni) + { + + // Check for Unicode or ASCII + + String ret = null; + + if (uni) + { + + // Word align the current buffer position + + m_pos = DataPacker.wordAlign(m_pos); + ret = DataPacker.getUnicodeString(m_smbbuf, m_pos, 255); + if (ret != null) + m_pos += (ret.length() * 2) + 2; + } + else + { + + // Unpack the ASCII string + + ret = DataPacker.getString(m_smbbuf, m_pos, 255); + if (ret != null) + m_pos += ret.length() + 1; + } + + // Return the string + + return ret; + } + + /** + * Unpack a string from the byte area + * + * @param len int + * @param uni boolean + * @return String + */ + public final String unpackString(int len, boolean uni) + { + + // Check for Unicode or ASCII + + String ret = null; + + if (uni) + { + + // Word align the current buffer position + + m_pos = DataPacker.wordAlign(m_pos); + ret = DataPacker.getUnicodeString(m_smbbuf, m_pos, len); + if (ret != null) + m_pos += (ret.length() * 2); + } + else + { + + // Unpack the ASCII string + + ret = DataPacker.getString(m_smbbuf, m_pos, len); + if (ret != null) + m_pos += ret.length(); + } + + // Return the string + + return ret; + } + + /** + * Check if there is more data in the byte area + * + * @return boolean + */ + public final boolean hasMoreData() + { + if (m_pos < m_endpos) + return true; + return false; + } + + /** + * Receive an SMB response packet. + * + * @param sess NetBIOS session to receive the SMB packet on. + * @exception java.io.IOException If an I/O error occurs. + */ + private final void ReceiveSMB(NetBIOSSession sess) throws java.io.IOException + { + + if (sess.Receive(m_smbbuf, RFCNetBIOSProtocol.TMO) >= MIN_RXLEN) + return; + + // Not enough data received for an SMB header + + throw new java.io.IOException("Short NetBIOS receive"); + } + + /** + * Set the secondary SMB command + * + * @param cmd Secondary SMB command code. + */ + public final void setAndXCommand(int cmd) + { + + // Set the chained command packet type + + m_smbbuf[ANDXCOMMAND] = (byte) cmd; + m_smbbuf[ANDXRESERVED] = (byte) 0; + + // If the AndX command is disabled clear the offset to the chained packet + + if (cmd == PacketType.NoChainedCommand) + setParameter(1, 0); + } + + /** + * Set the data byte count for this SMB packet + * + * @param cnt Data byte count. + */ + public final void setByteCount(int cnt) + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(cnt, m_smbbuf, offset); + } + + /** + * Set the data byte count for this SMB packet + */ + + public final void setByteCount() + { + int offset = getByteOffset() - 2; + int len = m_pos - getByteOffset(); + DataPacker.putIntelShort(len, m_smbbuf, offset); + } + + /** + * Set the data byte area in the SMB packet + * + * @param byts Byte array containing the data to be copied to the SMB packet. + */ + public final void setBytes(byte[] byts) + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(byts.length, m_smbbuf, offset); + + offset += 2; + + for (int idx = 0; idx < byts.length; m_smbbuf[offset + idx] = byts[idx++]) + ; + } + + /** + * Set the SMB command + * + * @param cmd SMB command code + */ + public final void setCommand(int cmd) + { + m_pkttype = cmd; + m_smbbuf[COMMAND] = (byte) cmd; + } + + /** + * Set the SMB error class. + * + * @param cl SMB error class. + */ + public final void setErrorClass(int cl) + { + m_smbbuf[ERRORCLASS] = (byte) (cl & 0xFF); + } + + /** + * Set the SMB error code + * + * @param sts SMB error code. + */ + public final void setErrorCode(int sts) + { + m_smbbuf[ERROR] = (byte) (sts & 0xFF); + } + + /** + * Set the SMB flags value. + * + * @param flg SMB flags value. + */ + public final void setFlags(int flg) + { + m_smbbuf[FLAGS] = (byte) flg; + } + + /** + * Set the SMB flags2 value. + * + * @param flg SMB flags2 value. + */ + public final void setFlags2(int flg) + { + DataPacker.putIntelShort(flg, m_smbbuf, FLAGS2); + } + + /** + * Set the multiplex identifier. + * + * @param mid Multiplex identifier + */ + public final void setMultiplexId(int mid) + { + DataPacker.putIntelShort(mid, m_smbbuf, MID); + } + + /** + * Set the specified parameter word. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + public final void setParameter(int idx, int val) + { + int pos = WORDCNT + (2 * idx) + 1; + DataPacker.putIntelShort(val, m_smbbuf, pos); + } + + /** + * Set the specified parameter words. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + + public final void setParameterLong(int idx, int val) + { + int pos = WORDCNT + (2 * idx) + 1; + DataPacker.putIntelInt(val, m_smbbuf, pos); + } + + /** + * Set the parameter count + * + * @param cnt Parameter word count. + */ + public final void setParameterCount(int cnt) + { + m_smbbuf[WORDCNT] = (byte) cnt; + } + + /** + * Set the process identifier value (PID). + * + * @param pid Process identifier value. + */ + public final void setProcessId(int pid) + { + DataPacker.putIntelShort(pid, m_smbbuf, PID); + } + + /** + * Set the packet sequence number, for connectionless commands. + * + * @param seq Sequence number. + */ + public final void setSeqNo(int seq) + { + DataPacker.putIntelShort(seq, m_smbbuf, SEQNO); + } + + /** + * Set the session id. + * + * @param sid Session id. + */ + public final void setSID(int sid) + { + DataPacker.putIntelShort(sid, m_smbbuf, SID); + } + + /** + * Set the tree identifier (TID) + * + * @param tid Tree identifier value. + */ + public final void setTreeId(int tid) + { + DataPacker.putIntelShort(tid, m_smbbuf, TID); + } + + /** + * Set the user identifier (UID) + * + * @param uid User identifier value. + */ + public final void setUserId(int uid) + { + DataPacker.putIntelShort(uid, m_smbbuf, UID); + } + + /** + * Align the byte area pointer on an int (32bit) boundary + */ + public final void alignBytePointer() + { + m_pos = DataPacker.longwordAlign(m_pos); + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking data items from the packet + */ + public final void resetBytePointer() + { + m_pos = getByteOffset(); + m_endpos = m_pos + getByteCount(); + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking data items from the packet, and + * align the buffer on an int (32bit) boundary + */ + public final void resetBytePointerAlign() + { + m_pos = DataPacker.longwordAlign(getByteOffset()); + m_endpos = m_pos + getByteCount(); + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking paramaters from the packet + */ + public final void resetParameterPointer() + { + m_pos = PARAMWORDS; + } + + /** + * Set the unpack pointer to the specified offset, for AndX processing + * + * @param off int + * @param len int + */ + public final void setBytePointer(int off, int len) + { + m_pos = off; + m_endpos = m_pos + len; + } + + /** + * Skip a number of bytes in the parameter/byte area + * + * @param cnt int + */ + public final void skipBytes(int cnt) + { + m_pos += cnt; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBServer.java b/source/java/org/alfresco/filesys/smb/server/SMBServer.java new file mode 100644 index 0000000000..b9ea78686d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBServer.java @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; +import java.util.Vector; + +import org.alfresco.filesys.netbios.NetworkSettings; +import org.alfresco.filesys.server.ServerListener; +import org.alfresco.filesys.server.SrvSessionList; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.auth.UserAccountList; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.server.core.InvalidDeviceInterfaceException; +import org.alfresco.filesys.server.core.ShareType; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.NetworkFileServer; +import org.alfresco.filesys.smb.DialectSelector; +import org.alfresco.filesys.smb.SMBException; +import org.alfresco.filesys.smb.ServerType; +import org.alfresco.filesys.smb.mailslot.HostAnnouncer; +import org.alfresco.filesys.smb.server.win32.Win32NetBIOSLanaMonitor; +import org.alfresco.filesys.smb.server.win32.Win32NetBIOSSessionSocketHandler; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

    + * Creates an SMB server with the specified host name. + *

    + * The server can optionally announce itself so that it will appear under the Network Neighborhood, + * by enabling the host announcer in the server configuration or using the enableAnnouncer() method. + */ +public class SMBServer extends NetworkFileServer implements Runnable +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Server version + + private static final String ServerVersion = "3.5.1"; + + // Server thread + + private Thread m_srvThread; + + // Session socket handlers (NetBIOS over TCP/IP, native SMB and/or Win32 NetBIOS) + + private Vector m_sessionHandlers; + + // Host announcers, server will appear under Network Neighborhood + + private Vector m_hostAnnouncers; + + // Active session list + + private SrvSessionList m_sessions; + + // Server type flags, used when announcing the host + + private int m_srvType = ServerType.WorkStation + ServerType.Server; + + // Next available session id + + private int m_sessId; + + // Server shutdown flag and server active flag + + private boolean m_shutdown = false; + private boolean m_active = false; + + /** + * Create an SMB server using the specified configuration. + * + * @param serviceRegistry repository connection + * @param cfg ServerConfiguration + */ + public SMBServer(ServerConfiguration cfg) throws IOException + { + super("SMB", cfg); + + // Call the common constructor + CommonConstructor(); + } + + /** + * Add a shared device to the server. + * + * @param shr Shared device to be added to the server. + * @return True if the share was added successfully, else false. + */ + public final synchronized boolean addShare(SharedDevice shr) + { + + // For disk devices check if the shared device is read-only, this should also check if the + // shared device + // path actully exists. + + if (shr.getType() == ShareType.DISK) + { + + // Check if the disk device is read-only + + checkReadOnly(shr); + } + + // Add the share to the shared device list + + boolean sts = getConfiguration().getShares().addShare(shr); + + // Debug + + if (logger.isInfoEnabled()) + logger.info("[SMB] Add Share " + shr.toString() + " : " + sts); + + // Return the add share status + + return sts; + } + + /** + * Add a session handler + * + * @param sessHandler SessionSocketHandler + */ + public final void addSessionHandler(SessionSocketHandler handler) + { + + // Check if the session handler list has been allocated + + if (m_sessionHandlers == null) + m_sessionHandlers = new Vector(); + + // Add the session handler + + m_sessionHandlers.addElement(handler); + } + + /** + * Add a host announcer + * + * @param announcer HostAnnouncer + */ + public final void addHostAnnouncer(HostAnnouncer announcer) + { + + // Check if the host announcer list has been allocated + + if (m_hostAnnouncers == null) + m_hostAnnouncers = new Vector(); + + // Add the host announcer + + m_hostAnnouncers.addElement(announcer); + } + + /** + * Add a new session to the server + * + * @param sess SMBSrvSession + */ + public final void addSession(SMBSrvSession sess) + { + + // Add the session to the session list + + m_sessions.addSession(sess); + + // Propagate the debug settings to the new session + + sess.setDebug(getConfiguration().getSessionDebugFlags()); + } + + /** + * Check if the disk share is read-only. + * + * @param shr SharedDevice + */ + protected final void checkReadOnly(SharedDevice shr) + { + + // For disk devices check if the shared device is read-only, this should also check if the + // shared device + // path actully exists. + + if (shr.getType() == ShareType.DISK) + { + + // Check if the disk device is read-only + + try + { + + // Get the device interface for the shared device + + DiskInterface disk = (DiskInterface) shr.getInterface(); + if (disk.isReadOnly(null, shr.getContext())) + { + + // The disk is read-only, mark the share as read-only + + int attr = shr.getAttributes(); + if ((attr & SharedDevice.ReadOnly) == 0) + attr += SharedDevice.ReadOnly; + shr.setAttributes(attr); + + // Debug + + if (logger.isInfoEnabled()) + logger.info("[SMB] Add Share " + shr.toString() + " : isReadOnly"); + } + } + catch (InvalidDeviceInterfaceException ex) + { + + // Shared device interface error + + if (logger.isInfoEnabled()) + logger.info("[SMB] Add Share " + shr.toString() + " : " + ex.toString()); + return; + } + catch (FileNotFoundException ex) + { + + // Shared disk device local path does not exist + + if (logger.isInfoEnabled()) + logger.info("[SMB] Add Share " + shr.toString() + " : " + ex.toString()); + return; + } + catch (IOException ex) + { + + // Shared disk device access error + + if (logger.isInfoEnabled()) + logger.info("[SMB] Add Share " + shr.toString() + " : " + ex.toString()); + return; + } + } + } + + /** + * Common constructor code. + */ + private void CommonConstructor() throws IOException + { + + // Set the server version + + setVersion(ServerVersion); + + // Create the session socket handler list + + m_sessionHandlers = new Vector(); + + // Create the active session list + + m_sessions = new SrvSessionList(); + + // Set the global domain name + + NetworkSettings.setDomain(getConfiguration().getDomainName()); + NetworkSettings.setBroadcastMask(getConfiguration().getBroadcastMask()); + } + + /** + * Close the host announcer, if enabled + */ + protected void closeHostAnnouncers() + { + + // Check if there are active host announcers + + if (m_hostAnnouncers != null) + { + + // Shutdown the host announcers + + for (int i = 0; i < m_hostAnnouncers.size(); i++) + { + + // Get the current host announcer from the active list + + HostAnnouncer announcer = (HostAnnouncer) m_hostAnnouncers.elementAt(i); + + // Shutdown the host announcer + + announcer.shutdownAnnouncer(); + } + } + } + + /** + * Close the session handlers + */ + protected void closeSessionHandlers() + { + + // Close the session handlers + + for (SessionSocketHandler handler : m_sessionHandlers) + { + + // Request the handler to shutdown + + handler.shutdownRequest(); + } + + // Clear the session handler list + + m_sessionHandlers.removeAllElements(); + } + + /** + * Delete the specified shared device from the server. + * + * @param name String Name of the shared resource to remove from the server. + * @return SharedDevice that has been removed from the server, else null. + */ + public final synchronized SharedDevice deleteShare(String name) + { + return getConfiguration().getShares().deleteShare(name); + } + + /** + * Delete temporary shares created by the share mapper for the specified session + * + * @param sess SMBSrvSession + */ + public final void deleteTemporaryShares(SMBSrvSession sess) + { + + // Delete temporary shares via the share mapper + + getConfiguration().getShareMapper().deleteShares(sess); + } + + /** + * Return an enumeration to allow the shared devices to be listed. + * + * @return java.util.Enumeration + */ + public final Enumeration enumerateShares() + { + return getConfiguration().getShares().enumerateShares(); + } + + /** + * Return the server comment. + * + * @return java.lang.String + */ + public final String getComment() + { + return getConfiguration().getComment(); + } + + /** + * Return the server type flags. + * + * @return int + */ + public final int getServerType() + { + return m_srvType; + } + + /** + * Return the per session debug flag settings. + */ + public final int getSessionDebug() + { + return getConfiguration().getSessionDebugFlags(); + } + + /** + * Return the list of SMB dialects that this server supports. + * + * @return DialectSelector + */ + public final DialectSelector getSMBDialects() + { + return getConfiguration().getEnabledDialects(); + } + + /** + * Return the list of user accounts. + * + * @return UserAccountList + */ + public final UserAccountList getUserAccountList() + { + return getConfiguration().getUserAccounts(); + } + + /** + * Return the active session list + * + * @return SrvSessionList + */ + public final SrvSessionList getSessions() + { + return m_sessions; + } + + /** + * Start the SMB server. + */ + public void run() + { + + // Indicate that the server is active + + setActive(true); + + // Check if we are running under Windows + + boolean isWindows = isWindowsNTOnwards(); + + // Debug + + if (logger.isInfoEnabled()) + { + + // Dump the server name/version and Java runtime details + + logger.info("[SMB] SMB Server " + getServerName() + " starting"); + logger.info("[SMB] Version " + isVersion()); + logger.info("[SMB] Java VM " + System.getProperty("java.vm.version")); + logger.info("[SMB] OS " + System.getProperty("os.name") + ", version " + System.getProperty("os.version")); + + // Output the authenticator details + + if (getAuthenticator() != null) + { + String mode = getAuthenticator().getAccessMode() == SrvAuthenticator.SHARE_MODE ? "SHARE" : "USER"; + logger.info("[SMB] Using authenticator " + getAuthenticator().getClass().getName() + ", mode=" + mode); + + // Display the count of user accounts + + if (getUserAccountList() != null) + logger.info("[SMB] " + getUserAccountList().numberOfUsers() + " user accounts defined"); + else + logger.info("[SMB] No user accounts defined"); + } + + // Display the timezone offset/name + + if (getConfiguration().getTimeZone() != null) + logger.info("[SMB] Server timezone " + getConfiguration().getTimeZone() + ", offset from UTC = " + + getConfiguration().getTimeZoneOffset() / 60 + "hrs"); + else + logger.info("[SMB] Server timezone offset = " + getConfiguration().getTimeZoneOffset() / 60 + "hrs"); + + // Dump the share list + + logger.info("[SMB] Shares:"); + Enumeration enm = getFullShareList(getServerName(), null).enumerateShares(); + + while (enm.hasMoreElements()) + { + SharedDevice share = enm.nextElement(); + logger.info("[SMB] " + share.toString() + " " + share.getContext().toString()); + } + } + + // Create a server socket to listen for incoming session requests + + try + { + + // Add the IPC$ named pipe shared device + + AdminSharedDevice admShare = new AdminSharedDevice(); + addShare(admShare); + + // Clear the server shutdown flag + + m_shutdown = false; + + // Get the list of IP addresses the server is bound to + + getServerIPAddresses(); + + // Check if the socket connection debug flag is enabled + + boolean sockDbg = false; + + if ((getSessionDebug() & SMBSrvSession.DBG_SOCKET) != 0) + sockDbg = true; + + // Create the NetBIOS session socket handler, if enabled + + if (getConfiguration().hasNetBIOSSMB()) + { + + // Create the TCP/IP NetBIOS SMB/CIFS session handler(s), and host announcer(s) if + // enabled + + NetBIOSSessionSocketHandler.createSessionHandlers(this, sockDbg); + } + + // Create the TCP/IP SMB session socket handler, if enabled + + if (getConfiguration().hasTcpipSMB()) + { + + // Create the TCP/IP native SMB session handler(s) + + TcpipSMBSessionSocketHandler.createSessionHandlers(this, sockDbg); + } + + // Create the Win32 NetBIOS session handler, if enabled + + if (getConfiguration().hasWin32NetBIOS()) + { + + // Only enable if running under Windows + + if (isWindows == true) + { + + // Create the Win32 NetBIOS SMB handler(s), and host announcer(s) if enabled + + Win32NetBIOSSessionSocketHandler.createSessionHandlers(this, sockDbg); + } + } + + // Check if there are any session handlers installed, if not then close the server + + if (m_sessionHandlers.size() > 0 || getConfiguration().hasWin32NetBIOS()) + { + + // Wait for incoming connection requests + + while (m_shutdown == false) + { + + // Sleep for a while + + try + { + Thread.sleep(1000L); + } + catch (InterruptedException ex) + { + } + } + } + else if (logger.isInfoEnabled()) + { + + // DEBUG + + logger.info("[SMB] No valid session handlers, server closing"); + } + } + catch (SMBException ex) + { + + // Output the exception + + logger.error("SMB server error", ex); + + // Store the error, fire a server error event + + setException(ex); + fireServerEvent(ServerListener.ServerError); + } + catch (Exception ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (m_shutdown == false) + { + logger.error("[SMB] Server error : ", ex); + + // Store the error, fire a server error event + + setException(ex); + fireServerEvent(ServerListener.ServerError); + } + } + + // Debug + + if (logger.isInfoEnabled()) + logger.info("[SMB] SMB Server shutting down ..."); + + // Close the host announcer and session handlers + + closeHostAnnouncers(); + closeSessionHandlers(); + + // Shutdown the Win32 NetBIOS LANA monitor, if enabled + + if (isWindows && Win32NetBIOSLanaMonitor.getLanaMonitor() != null) + Win32NetBIOSLanaMonitor.getLanaMonitor().shutdownRequest(); + + // Indicate that the server is not active + + setActive(false); + fireServerEvent(ServerListener.ServerShutdown); + } + + /** + * Notify the server that a session has been closed. + * + * @param sess SMBSrvSession + */ + protected final void sessionClosed(SMBSrvSession sess) + { + + // Remove the session from the active session list + + m_sessions.removeSession(sess); + + // Notify session listeners that a session has been closed + + fireSessionClosedEvent(sess); + } + + /** + * Notify the server that a user has logged on. + * + * @param sess SMBSrvSession + */ + protected final void sessionLoggedOn(SMBSrvSession sess) + { + + // Notify session listeners that a user has logged on. + + fireSessionLoggedOnEvent(sess); + } + + /** + * Notify the server that a session has been closed. + * + * @param sess SMBSrvSession + */ + protected final void sessionOpened(SMBSrvSession sess) + { + + // Notify session listeners that a session has been closed + + fireSessionOpenEvent(sess); + } + + /** + * Shutdown the SMB server + * + * @param immediate boolean + */ + public final void shutdownServer(boolean immediate) + { + + // Indicate that the server is closing + + m_shutdown = true; + + try + { + + // Close the session handlers + + closeSessionHandlers(); + } + catch (Exception ex) + { + } + + // Close the active sessions + + Enumeration enm = m_sessions.enumerate(); + + while (enm.hasMoreElements()) + { + + // Get the session id and associated session + + Integer sessId = enm.nextElement(); + SMBSrvSession sess = (SMBSrvSession) m_sessions.findSession(sessId); + + // Inform listeners that the session has been closed + + fireSessionClosedEvent(sess); + + // Close the session + + sess.closeSession(); + } + + // Wait for the main server thread to close + + if (m_srvThread != null) + { + + try + { + m_srvThread.join(3000); + } + catch (Exception ex) + { + } + } + + // Fire a shutdown notification event + + fireServerEvent(ServerListener.ServerShutdown); + } + + /** + * Start the SMB server in a seperate thread + */ + public void startServer() + { + + // Create a seperate thread to run the SMB server + + m_srvThread = new Thread(this); + m_srvThread.setName("SMB Server"); + m_srvThread.setDaemon(true); + + m_srvThread.start(); + + // Fire a server startup event + + fireServerEvent(ServerListener.ServerStartup); + } + + /** + * Determine if we are running under Windows NT onwards + * + * @return boolean + */ + private final boolean isWindowsNTOnwards() + { + + // Get the operating system name property + + String osName = System.getProperty("os.name"); + + if (osName.startsWith("Windows")) + { + if (osName.endsWith("95") || osName.endsWith("98") || osName.endsWith("ME")) + { + + // Windows 95-ME + + return false; + } + + // Looks like Windows NT onwards + + return true; + } + + // Not Windows + + return false; + } + + /** + * Get the list of local IP addresses + */ + private final void getServerIPAddresses() + { + + try + { + + // Get the local IP address list + + Enumeration enm = NetworkInterface.getNetworkInterfaces(); + Vector addrList = new Vector(); + + while (enm.hasMoreElements()) + { + + // Get the current network interface + + NetworkInterface ni = enm.nextElement(); + + // Get the address list for the current interface + + Enumeration addrs = ni.getInetAddresses(); + + while (addrs.hasMoreElements()) + addrList.add(addrs.nextElement()); + } + + // Convert the vector of addresses to an array + + if (addrList.size() > 0) + { + + // Convert the address vector to an array + + InetAddress[] inetAddrs = new InetAddress[addrList.size()]; + + // Copy the address details to the array + + for (int i = 0; i < addrList.size(); i++) + inetAddrs[i] = (InetAddress) addrList.elementAt(i); + + // Set the server IP address list + + setServerAddresses(inetAddrs); + } + } + catch (Exception ex) + { + + // DEBUG + + logger.error("[SMB] Error getting local IP addresses", ex); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBSrvException.java b/source/java/org/alfresco/filesys/smb/server/SMBSrvException.java new file mode 100644 index 0000000000..882333699e --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBSrvException.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.smb.SMBErrorText; + +/** + * SMB exception class + *

    + * This class holds the detail of an SMB network error. The SMB error class and error code are + * available to give extra detail about the error condition. + */ +public class SMBSrvException extends Exception +{ + private static final long serialVersionUID = 3976733662123341368L; + + // SMB error class + + protected int m_errorclass; + + // SMB error code + + protected int m_errorcode; + + /** + * Construct an SMB exception with the specified error class/error code. + */ + public SMBSrvException(int errclass, int errcode) + { + super(SMBErrorText.ErrorString(errclass, errcode)); + m_errorclass = errclass; + m_errorcode = errcode; + } + + /** + * Construct an SMB exception with the specified error class/error code and additional text + * error message. + */ + public SMBSrvException(int errclass, int errcode, String msg) + { + super(msg); + m_errorclass = errclass; + m_errorcode = errcode; + } + + /** + * Construct an SMB exception using the error class/error code in the SMB packet + */ + protected SMBSrvException(SMBSrvPacket pkt) + { + super(SMBErrorText.ErrorString(pkt.getErrorClass(), pkt.getErrorCode())); + m_errorclass = pkt.getErrorClass(); + m_errorcode = pkt.getErrorCode(); + } + + /** + * Return the error class for this SMB exception. + * + * @return SMB error class. + */ + public int getErrorClass() + { + return m_errorclass; + } + + /** + * Return the error code for this SMB exception + * + * @return SMB error code + */ + public int getErrorCode() + { + return m_errorcode; + } + + /** + * Return the error text for the SMB exception + * + * @return Error text string. + */ + public String getErrorText() + { + return SMBErrorText.ErrorString(m_errorclass, m_errorcode); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBSrvPacket.java b/source/java/org/alfresco/filesys/smb/server/SMBSrvPacket.java new file mode 100644 index 0000000000..42b44ae197 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBSrvPacket.java @@ -0,0 +1,1756 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.DataOutputStream; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBErrorText; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.HexDump; + +/** + * SMB packet type class + */ +public class SMBSrvPacket +{ + + // Protocol type, either NetBIOS or TCP/IP native SMB + // + // All protocols reserve a 4 byte header, header is not used by Win32 NetBIOS + + public static final int PROTOCOL_NETBIOS = 0; + public static final int PROTOCOL_TCPIP = 1; + public static final int PROTOCOL_WIN32NETBIOS = 2; + + // SMB packet offsets, assuming an RFC NetBIOS transport + + public static final int SIGNATURE = RFCNetBIOSProtocol.HEADER_LEN; + public static final int COMMAND = 4 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ERRORCODE = 5 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ERRORCLASS = 5 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ERROR = 7 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int FLAGS = 9 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int FLAGS2 = 10 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int PIDHIGH = 12 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int SID = 18 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int SEQNO = 20 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int TID = 24 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int PID = 26 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int UID = 28 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int MID = 30 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int WORDCNT = 32 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ANDXCOMMAND = 33 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int ANDXRESERVED = 34 + RFCNetBIOSProtocol.HEADER_LEN; + public static final int PARAMWORDS = 33 + RFCNetBIOSProtocol.HEADER_LEN; + + // SMB packet header length for a transaction type request + + public static final int TRANS_HEADERLEN = 66 + RFCNetBIOSProtocol.HEADER_LEN; + + // Minimum receive length for a valid SMB packet + + public static final int MIN_RXLEN = 32; + + // Default buffer size to allocate for SMB packets + + public static final int DEFAULT_BUFSIZE = 4096; + + // Flag bits + + public static final int FLG_SUBDIALECT = 0x01; + public static final int FLG_CASELESS = 0x08; + public static final int FLG_CANONICAL = 0x10; + public static final int FLG_OPLOCK = 0x20; + public static final int FLG_NOTIFY = 0x40; + public static final int FLG_RESPONSE = 0x80; + + // Flag2 bits + + public static final int FLG2_LONGFILENAMES = 0x0001; + public static final int FLG2_EXTENDEDATTRIB = 0x0002; + public static final int FLG2_READIFEXE = 0x2000; + public static final int FLG2_LONGERRORCODE = 0x4000; + public static final int FLG2_UNICODE = 0x8000; + + // Security mode bits + + public static final int SEC_USER = 0x0001; + public static final int SEC_ENCRYPT = 0x0002; + + // Raw mode bits + + public static final int RAW_READ = 0x0001; + public static final int RAW_WRITE = 0x0002; + + // No chained AndX command indicator + + public static final int NO_ANDX_CMD = 0x00FF; + + // SMB packet buffer + + private byte[] m_smbbuf; + + // Received data length (actual buffer used) + + private int m_rxLen; + + // Packet type + + private int m_pkttype; + + // Current byte area pack/unpack position + + protected int m_pos; + protected int m_endpos; + + /** + * Default constructor + */ + + public SMBSrvPacket() + { + m_smbbuf = new byte[DEFAULT_BUFSIZE]; + InitializeBuffer(); + } + + /** + * Construct an SMB packet using the specified packet buffer. + * + * @param buf SMB packet buffer. + */ + + public SMBSrvPacket(byte[] buf) + { + m_smbbuf = buf; + } + + /** + * Construct an SMB packet of the specified size. + * + * @param siz Size of SMB packet buffer to allocate. + */ + + public SMBSrvPacket(int siz) + { + m_smbbuf = new byte[siz]; + InitializeBuffer(); + } + + /** + * Copy constructor. + * + * @param buf SMB packet buffer. + */ + + public SMBSrvPacket(SMBSrvPacket pkt) + { + + // Create a packet buffer of the same size + + m_smbbuf = new byte[pkt.getBuffer().length]; + + // Copy the data from the specified packet + + System.arraycopy(pkt.getBuffer(), 0, m_smbbuf, 0, m_smbbuf.length); + } + + /** + * Copy constructor. + * + * @param buf SMB packet buffer. + * @param len Length of packet to be copied + */ + + public SMBSrvPacket(SMBSrvPacket pkt, int len) + { + + // Create a packet buffer of the same size + + m_smbbuf = new byte[pkt.getBuffer().length]; + + // Copy the data from the specified packet + + System.arraycopy(pkt.getBuffer(), 0, m_smbbuf, 0, len); + } + + /** + * Check the SMB AndX command for the required minimum parameter count and byte count. + * + * @param off Offset to the AndX command within the SMB packet. + * @param reqWords Minimum number of parameter words expected. + * @param reqBytes Minimum number of bytes expected. + * @return boolean True if the packet passes the checks, else false. + */ + public final boolean checkAndXPacketIsValid(int off, int reqWords, int reqBytes) + { + + // Check the received parameter word count + + if (getAndXParameterCount(off) < reqWords || getAndXByteCount(off) < reqBytes) + return false; + + // Initial SMB packet checks passed + + return true; + } + + /** + * Check the SMB packet for a valid SMB signature, and the required minimum parameter count and + * byte count. + * + * @param reqWords Minimum number of parameter words expected. + * @param reqBytes Minimum number of bytes expected. + * @return boolean True if the packet passes the checks, else false. + */ + public final boolean checkPacketIsValid(int reqWords, int reqBytes) + { + + // Check for the SMB signature block + + if (m_smbbuf[SIGNATURE] != (byte) 0xFF || m_smbbuf[SIGNATURE + 1] != 'S' || m_smbbuf[SIGNATURE + 2] != 'M' + || m_smbbuf[SIGNATURE + 3] != 'B') + return false; + + // Check the received parameter word count + + if (getParameterCount() < reqWords || getByteCount() < reqBytes) + return false; + + // Initial SMB packet checks passed + + return true; + } + + /** + * Check the SMB packet has a valid SMB signature. + * + * @return boolean True if the SMB signature is valid, else false. + */ + public final boolean checkPacketSignature() + { + + // Check for the SMB signature block + + if (m_smbbuf[SIGNATURE] == (byte) 0xFF && m_smbbuf[SIGNATURE + 1] == 'S' && m_smbbuf[SIGNATURE + 2] == 'M' + && m_smbbuf[SIGNATURE + 3] == 'B') + return true; + + // Invalid SMB packet format + + return false; + } + + /** + * Clear the data byte count + */ + + public final void clearBytes() + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort((short) 0, m_smbbuf, offset); + } + + /** + * Dump the SMB packet to the debug stream + */ + + public final void DumpPacket() + { + DumpPacket(false); + } + + /** + * Dump the SMB packet to the debug stream + * + * @param dumpAll boolean + */ + + public final void DumpPacket(boolean dumpAll) + { + + // Dump the command type + + int pCount = getParameterCount(); + System.out.print("** SMB Packet Type: " + getPacketTypeString()); + + // Check if this is a response packet + + if (isResponse()) + System.out.println(" [Response]"); + else + System.out.println(); + + // Dump flags/secondary flags + + if (true) + { + + // Dump the packet length + + System.out.println("** SMB Packet Dump"); + System.out.println("Packet Length : " + getLength()); + System.out.println("Byte Offset: " + getByteOffset() + ", Byte Count: " + getByteCount()); + + // Dump the flags + + System.out.println("Flags: " + Integer.toBinaryString(getFlags())); + System.out.println("Flags2: " + Integer.toBinaryString(getFlags2())); + + // Dump various ids + + System.out.println("TID: " + getTreeId()); + System.out.println("PID: " + getProcessId()); + System.out.println("UID: " + getUserId()); + System.out.println("MID: " + getMultiplexId()); + + // Dump parameter words/count + + System.out.println("Parameter Words: " + pCount); + StringBuffer str = new StringBuffer(); + + for (int i = 0; i < pCount; i++) + { + str.setLength(0); + str.append(" P"); + str.append(Integer.toString(i + 1)); + str.append(" = "); + str.append(Integer.toString(getParameter(i))); + while (str.length() < 16) + str.append(" "); + str.append("0x"); + str.append(Integer.toHexString(getParameter(i))); + System.out.println(str.toString()); + } + + // Response packet fields + + if (isResponse()) + { + + // Dump the error code + + System.out.println("Error: 0x" + Integer.toHexString(getErrorCode())); + System.out.print("Error Class: "); + + switch (getErrorClass()) + { + case SMBStatus.Success: + System.out.println("SUCCESS"); + break; + case SMBStatus.ErrDos: + System.out.println("ERRDOS"); + break; + case SMBStatus.ErrSrv: + System.out.println("ERRSRV"); + break; + case SMBStatus.ErrHrd: + System.out.println("ERRHRD"); + break; + case SMBStatus.ErrCmd: + System.out.println("ERRCMD"); + break; + default: + System.out.println("0x" + Integer.toHexString(getErrorClass())); + break; + } + + // Display the SMB error text + + System.out.print("Error Text: "); + System.out.println(SMBErrorText.ErrorString(getErrorClass(), getErrorCode())); + } + } + + // Dump the raw data + + if (true) + { + System.out.println("********** Raw SMB Data Dump **********"); + if (dumpAll) + HexDump.Dump(m_smbbuf, getLength(), 4); + else + HexDump.Dump(m_smbbuf, getLength() < 100 ? getLength() : 100, 4); + } + + System.out.println(); + System.out.flush(); + } + + /** + * Get the data byte count for the SMB AndX command. + * + * @param off Offset to the AndX command. + * @return Data byte count + */ + + public final int getAndXByteCount(int off) + { + + // Calculate the offset of the byte count + + int pos = off + 1 + (2 * getParameterCount()); + return (int) DataPacker.getIntelShort(m_smbbuf, pos); + } + + /** + * Get the AndX data byte area offset within the SMB packet + * + * @param off Offset to the AndX command. + * @return Data byte offset within the SMB packet. + */ + + public final int getAndXByteOffset(int off) + { + + // Calculate the offset of the byte buffer + + int pCnt = getAndXParameterCount(off); + int pos = off + (2 * pCnt) + 3; // parameter words + paramter count byte + byte data length + // word + return pos; + } + + /** + * Get the secondary command code + * + * @return Secondary command code + */ + + public final int getAndXCommand() + { + return (int) (m_smbbuf[ANDXCOMMAND] & 0xFF); + } + + /** + * Get an AndX parameter word from the SMB packet. + * + * @param off Offset to the AndX command. + * @param idx Parameter index (zero based). + * @return Parameter word value. + * @exception java.lang.IndexOutOfBoundsException If the parameter index is out of range. + */ + + public final int getAndXParameter(int off, int idx) throws java.lang.IndexOutOfBoundsException + { + + // Range check the parameter index + + if (idx > getAndXParameterCount(off)) + throw new java.lang.IndexOutOfBoundsException(); + + // Calculate the parameter word offset + + int pos = off + (2 * idx) + 1; + return (int) (DataPacker.getIntelShort(m_smbbuf, pos) & 0xFFFF); + } + + /** + * Get an AndX parameter integer from the SMB packet. + * + * @param off Offset to the AndX command. + * @param idx Parameter index (zero based). + * @return Parameter integer value. + * @exception java.lang.IndexOutOfBoundsException If the parameter index is out of range. + */ + + public final int getAndXParameterLong(int off, int idx) throws java.lang.IndexOutOfBoundsException + { + + // Range check the parameter index + + if (idx > getAndXParameterCount(off)) + throw new java.lang.IndexOutOfBoundsException(); + + // Calculate the parameter word offset + + int pos = off + (2 * idx) + 1; + return DataPacker.getIntelInt(m_smbbuf, pos); + } + + /** + * Get the AndX command parameter count. + * + * @param off Offset to the AndX command. + * @return Parameter word count. + */ + + public final int getAndXParameterCount(int off) + { + return (int) m_smbbuf[off]; + } + + /** + * Return the byte array used for the SMB packet + * + * @return Byte array used for the SMB packet. + */ + + public final byte[] getBuffer() + { + return m_smbbuf; + } + + /** + * Return the total buffer size available to the SMB request + * + * @return Total SMB buffer length available. + */ + + public final int getBufferLength() + { + return m_smbbuf.length - RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Get the data byte count for the SMB packet + * + * @return Data byte count + */ + + public final int getByteCount() + { + + // Calculate the offset of the byte count + + int pos = PARAMWORDS + (2 * getParameterCount()); + return (int) DataPacker.getIntelShort(m_smbbuf, pos); + } + + /** + * Get the data byte area offset within the SMB packet + * + * @return Data byte offset within the SMB packet. + */ + + public final int getByteOffset() + { + + // Calculate the offset of the byte buffer + + int pCnt = getParameterCount(); + int pos = WORDCNT + (2 * pCnt) + 3; + return pos; + } + + /** + * Get the SMB command + * + * @return SMB command code. + */ + + public final int getCommand() + { + return (int) (m_smbbuf[COMMAND] & 0xFF); + } + + /** + * Get the SMB error class + * + * @return SMB error class. + */ + + public final int getErrorClass() + { + return (int) m_smbbuf[ERRORCLASS] & 0xFF; + } + + /** + * Get the SMB error code + * + * @return SMB error code. + */ + + public final int getErrorCode() + { + return (int) m_smbbuf[ERROR] & 0xFF; + } + + /** + * Get the SMB flags value. + * + * @return SMB flags value. + */ + + public final int getFlags() + { + return (int) m_smbbuf[FLAGS] & 0xFF; + } + + /** + * Get the SMB flags2 value. + * + * @return SMB flags2 value. + */ + public final int getFlags2() + { + return (int) DataPacker.getIntelShort(m_smbbuf, FLAGS2); + } + + /** + * Calculate the total used packet length. + * + * @return Total used packet length. + */ + public final int getLength() + { + + // Get the length of the first command in the packet + + return (getByteOffset() + getByteCount()) - SIGNATURE; + } + + /** + * Calculate the total packet length, including header + * + * @return Total packet length. + */ + public final int getPacketLength() + { + + // Get the length of the first command in the packet + + return getByteOffset() + getByteCount(); + } + + /** + * Return the available buffer space for data bytes + * + * @return int + */ + public final int getAvailableLength() + { + return m_smbbuf.length - DataPacker.longwordAlign(getByteOffset()); + } + + /** + * Return the available buffer space for data bytes for the specified buffer length + * + * @param len int + * @return int + */ + public final int getAvailableLength(int len) + { + return len - DataPacker.longwordAlign(getByteOffset()); + } + + /** + * Get the long SMB error code + * + * @return Long SMB error code. + */ + public final int getLongErrorCode() + { + return DataPacker.getIntelInt(m_smbbuf, ERRORCODE); + } + + /** + * Get the multiplex identifier. + * + * @return Multiplex identifier. + */ + public final int getMultiplexId() + { + return DataPacker.getIntelShort(m_smbbuf, MID); + } + + /** + * Dump the packet type + * + * @return String + */ + public final String getPacketTypeString() + { + + String pktType = ""; + + switch (getCommand()) + { + case PacketType.Negotiate: + pktType = "NEGOTIATE"; + break; + case PacketType.SessionSetupAndX: + pktType = "SESSION_SETUP"; + break; + case PacketType.TreeConnect: + pktType = "TREE_CONNECT"; + break; + case PacketType.TreeConnectAndX: + pktType = "TREE_CONNECT_ANDX"; + break; + case PacketType.TreeDisconnect: + pktType = "TREE_DISCONNECT"; + break; + case PacketType.Search: + pktType = "SEARCH"; + break; + case PacketType.OpenFile: + pktType = "OPEN_FILE"; + break; + case PacketType.OpenAndX: + pktType = "OPEN_ANDX"; + break; + case PacketType.ReadFile: + pktType = "READ_FILE"; + break; + case PacketType.WriteFile: + pktType = "WRITE_FILE"; + break; + case PacketType.CloseFile: + pktType = "CLOSE_FILE"; + break; + case PacketType.CreateFile: + pktType = "CREATE_FILE"; + break; + case PacketType.GetFileAttributes: + pktType = "GET_FILE_INFO"; + break; + case PacketType.DiskInformation: + pktType = "GET_DISK_INFO"; + break; + case PacketType.CheckDirectory: + pktType = "CHECK_DIRECTORY"; + break; + case PacketType.RenameFile: + pktType = "RENAME_FILE"; + break; + case PacketType.DeleteDirectory: + pktType = "DELETE_DIRECTORY"; + break; + case PacketType.GetPrintQueue: + pktType = "GET_PRINT_QUEUE"; + break; + case PacketType.Transaction2: + pktType = "TRANSACTION2"; + break; + case PacketType.Transaction: + pktType = "TRANSACTION"; + break; + case PacketType.Transaction2Second: + pktType = "TRANSACTION2_SECONDARY"; + break; + case PacketType.TransactionSecond: + pktType = "TRANSACTION_SECONDARY"; + break; + case PacketType.Echo: + pktType = "ECHO"; + break; + case PacketType.QueryInformation2: + pktType = "QUERY_INFORMATION_2"; + break; + case PacketType.WriteAndClose: + pktType = "WRITE_AND_CLOSE"; + break; + case PacketType.SetInformation2: + pktType = "SET_INFORMATION_2"; + break; + case PacketType.FindClose2: + pktType = "FIND_CLOSE2"; + break; + case PacketType.LogoffAndX: + pktType = "LOGOFF_ANDX"; + break; + case PacketType.NTCancel: + pktType = "NTCANCEL"; + break; + case PacketType.NTCreateAndX: + pktType = "NTCREATE_ANDX"; + break; + case PacketType.NTTransact: + pktType = "NTTRANSACT"; + break; + case PacketType.NTTransactSecond: + pktType = "NTTRANSACT_SECONDARY"; + break; + case PacketType.ReadAndX: + pktType = "READ_ANDX"; + break; + default: + pktType = "0x" + Integer.toHexString(getCommand()); + break; + } + + // Return the packet type string + + return pktType; + } + + /** + * Get a parameter word from the SMB packet. + * + * @param idx Parameter index (zero based). + * @return Parameter word value. + * @exception java.lang.IndexOutOfBoundsException If the parameter index is out of range. + */ + + public final int getParameter(int idx) throws java.lang.IndexOutOfBoundsException + { + + // Range check the parameter index + + if (idx > getParameterCount()) + throw new java.lang.IndexOutOfBoundsException(); + + // Calculate the parameter word offset + + int pos = WORDCNT + (2 * idx) + 1; + return (int) (DataPacker.getIntelShort(m_smbbuf, pos) & 0xFFFF); + } + + /** + * Get the parameter count + * + * @return Parameter word count. + */ + + public final int getParameterCount() + { + return (int) m_smbbuf[WORDCNT]; + } + + /** + * Get the specified parameter words, as an int value. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + + public final int getParameterLong(int idx) + { + int pos = WORDCNT + (2 * idx) + 1; + return DataPacker.getIntelInt(m_smbbuf, pos); + } + + /** + * Get the process indentifier (PID) + * + * @return Process identifier value. + */ + public final int getProcessId() + { + return DataPacker.getIntelShort(m_smbbuf, PID); + } + + /** + * Get the actual received data length. + * + * @return int + */ + public final int getReceivedLength() + { + return m_rxLen; + } + + /** + * Get the session identifier (SID) + * + * @return Session identifier (SID) + */ + + public final int getSID() + { + return DataPacker.getIntelShort(m_smbbuf, SID); + } + + /** + * Get the tree identifier (TID) + * + * @return Tree identifier (TID) + */ + + public final int getTreeId() + { + return DataPacker.getIntelShort(m_smbbuf, TID); + } + + /** + * Get the user identifier (UID) + * + * @return User identifier (UID) + */ + + public final int getUserId() + { + return DataPacker.getIntelShort(m_smbbuf, UID); + } + + /** + * Determine if there is a secondary command in this packet. + * + * @return Secondary command code + */ + + public final boolean hasAndXCommand() + { + + // Check if there is a secondary command + + int andxCmd = getAndXCommand(); + + if (andxCmd != 0xFF && andxCmd != 0) + return true; + return false; + } + + /** + * Initialize the SMB packet buffer. + */ + + private final void InitializeBuffer() + { + + // Set the packet signature + + m_smbbuf[SIGNATURE] = (byte) 0xFF; + m_smbbuf[SIGNATURE + 1] = (byte) 'S'; + m_smbbuf[SIGNATURE + 2] = (byte) 'M'; + m_smbbuf[SIGNATURE + 3] = (byte) 'B'; + } + + /** + * Determine if this packet is an SMB response, or command packet + * + * @return true if this SMB packet is a response, else false + */ + + public final boolean isResponse() + { + int resp = getFlags(); + if ((resp & FLG_RESPONSE) != 0) + return true; + return false; + } + + /** + * Check if the response packet is valid, ie. type and flags + * + * @return true if the SMB packet is a response packet and the response is valid, else false. + */ + + public final boolean isValidResponse() + { + + // Check if this is a response packet, and the correct type of packet + + if (isResponse() && getCommand() == m_pkttype && this.getErrorClass() == SMBStatus.Success) + return true; + return false; + } + + /** + * Check if the packet contains ASCII or Unicode strings + * + * @return boolean + */ + public final boolean isUnicode() + { + return (getFlags2() & FLG2_UNICODE) != 0 ? true : false; + } + + /** + * Check if the packet is using caseless filenames + * + * @return boolean + */ + public final boolean isCaseless() + { + return (getFlags() & FLG_CASELESS) != 0 ? true : false; + } + + /** + * Check if long file names are being used + * + * @return boolean + */ + public final boolean isLongFileNames() + { + return (getFlags2() & FLG2_LONGFILENAMES) != 0 ? true : false; + } + + /** + * Check if long error codes are being used + * + * @return boolean + */ + public final boolean isLongErrorCode() + { + return (getFlags2() & FLG2_LONGERRORCODE) != 0 ? true : false; + } + + /** + * Pack a byte (8 bit) value into the byte area + * + * @param val byte + */ + public final void packByte(byte val) + { + m_smbbuf[m_pos++] = val; + } + + /** + * Pack a byte (8 bit) value into the byte area + * + * @param val int + */ + public final void packByte(int val) + { + m_smbbuf[m_pos++] = (byte) val; + } + + /** + * Pack the specified bytes into the byte area + * + * @param byts byte[] + * @param len int + */ + public final void packBytes(byte[] byts, int len) + { + for (int i = 0; i < len; i++) + m_smbbuf[m_pos++] = byts[i]; + } + + /** + * Pack a string using either ASCII or Unicode into the byte area + * + * @param str String + * @param uni boolean + */ + public final void packString(String str, boolean uni) + { + + // Check for Unicode or ASCII + + if (uni) + { + + // Word align the buffer position, pack the Unicode string + + m_pos = DataPacker.wordAlign(m_pos); + DataPacker.putUnicodeString(str, m_smbbuf, m_pos, true); + m_pos += (str.length() * 2) + 2; + } + else + { + + // Pack the ASCII string + + DataPacker.putString(str, m_smbbuf, m_pos, true); + m_pos += str.length() + 1; + } + } + + /** + * Pack a string using either ASCII or Unicode into the byte area + * + * @param str String + * @param uni boolean + * @param nul boolean + */ + public final void packString(String str, boolean uni, boolean nul) + { + + // Check for Unicode or ASCII + + if (uni) + { + + // Word align the buffer position, pack the Unicode string + + m_pos = DataPacker.wordAlign(m_pos); + DataPacker.putUnicodeString(str, m_smbbuf, m_pos, nul); + m_pos += (str.length() * 2); + if (nul == true) + m_pos += 2; + } + else + { + + // Pack the ASCII string + + DataPacker.putString(str, m_smbbuf, m_pos, true); + m_pos += str.length(); + if (nul == true) + m_pos++; + } + } + + /** + * Pack a word (16 bit) value into the byte area + * + * @param val int + */ + public final void packWord(int val) + { + DataPacker.putIntelShort(val, m_smbbuf, m_pos); + m_pos += 2; + } + + /** + * Pack an integer (32 bit) value into the byte area + * + * @param val int + */ + public final void packInt(int val) + { + DataPacker.putIntelInt(val, m_smbbuf, m_pos); + m_pos += 4; + } + + /** + * Pack a long integer (64 bit) value into the byte area + * + * @param val long + */ + public final void packLong(long val) + { + DataPacker.putIntelLong(val, m_smbbuf, m_pos); + m_pos += 8; + } + + /** + * Return the current byte area buffer position + * + * @return int + */ + public final int getPosition() + { + return m_pos; + } + + /** + * Unpack a byte value from the byte area + * + * @return int + */ + public final int unpackByte() + { + return (int) m_smbbuf[m_pos++]; + } + + /** + * Unpack a block of bytes from the byte area + * + * @param len int + * @return byte[] + */ + public final byte[] unpackBytes(int len) + { + if (len <= 0) + return null; + + byte[] buf = new byte[len]; + System.arraycopy(m_smbbuf, m_pos, buf, 0, len); + m_pos += len; + return buf; + } + + /** + * Unpack a word (16 bit) value from the byte area + * + * @return int + */ + public final int unpackWord() + { + int val = DataPacker.getIntelShort(m_smbbuf, m_pos); + m_pos += 2; + return val; + } + + /** + * Unpack an integer (32 bit) value from the byte area + * + * @return int + */ + public final int unpackInt() + { + int val = DataPacker.getIntelInt(m_smbbuf, m_pos); + m_pos += 4; + return val; + } + + /** + * Unpack a long integer (64 bit) value from the byte area + * + * @return long + */ + public final long unpackLong() + { + long val = DataPacker.getIntelLong(m_smbbuf, m_pos); + m_pos += 8; + return val; + } + + /** + * Unpack a string from the byte area + * + * @param uni boolean + * @return String + */ + public final String unpackString(boolean uni) + { + + // Check for Unicode or ASCII + + String ret = null; + + if (uni) + { + + // Word align the current buffer position + + m_pos = DataPacker.wordAlign(m_pos); + ret = DataPacker.getUnicodeString(m_smbbuf, m_pos, 255); + if (ret != null) + m_pos += (ret.length() * 2) + 2; + } + else + { + + // Unpack the ASCII string + + ret = DataPacker.getString(m_smbbuf, m_pos, 255); + if (ret != null) + m_pos += ret.length() + 1; + } + + // Return the string + + return ret; + } + + /** + * Check if there is more data in the byte area + * + * @return boolean + */ + public final boolean hasMoreData() + { + if (m_pos < m_endpos) + return true; + return false; + } + + /** + * Send the SMB response packet. + * + * @param out Output stream associated with the session socket. + * @param proto Protocol type, either PROTOCOL_NETBIOS or PROTOCOL_TCPIP + * @exception java.io.IOException If an I/O error occurs. + */ + public final void SendResponseSMB(DataOutputStream out, int proto) throws java.io.IOException + { + + // Use the packet length + + int siz = getLength(); + SendResponseSMB(out, proto, siz); + } + + /** + * Send the SMB response packet. + * + * @param out Output stream associated with the session socket. + * @param proto Protocol type, either PROTOCOL_NETBIOS or PROTOCOL_TCPIP + * @param len Packet length + * @exception java.io.IOException If an I/O error occurs. + */ + public final void SendResponseSMB(DataOutputStream out, int proto, int len) throws java.io.IOException + { + + // Make sure the response flag is set + + int flg = getFlags(); + if ((flg & FLG_RESPONSE) == 0) + setFlags(flg + FLG_RESPONSE); + + // NetBIOS SMB protocol + + if (proto == PROTOCOL_NETBIOS) + { + + // Fill in the NetBIOS message header, this is already allocated as + // part of the users buffer. + + m_smbbuf[0] = (byte) RFCNetBIOSProtocol.SESSION_MESSAGE; + m_smbbuf[1] = (byte) 0; + + DataPacker.putShort((short) len, m_smbbuf, 2); + } + else + { + + // TCP/IP native SMB + + DataPacker.putInt(len, m_smbbuf, 0); + } + + // Output the data packet + + len += RFCNetBIOSProtocol.HEADER_LEN; + out.write(m_smbbuf, 0, len); + } + + /** + * Send a success SMB response packet. + * + * @param out Output stream associated with the session socket. + * @param proto Protocol type, either PROTOCOL_NETBIOS or PROTOCOL_TCPIP + * @exception java.io.IOException If an I/O error occurs. + */ + + public final void SendSuccessSMB(DataOutputStream out, int proto) throws java.io.IOException + { + + // Clear the parameter and byte counts + + setParameterCount(0); + setByteCount(0); + + // Send the success response + + SendResponseSMB(out, proto); + } + + /** + * Set the AndX data byte count for this SMB packet. + * + * @param off AndX command offset. + * @param cnt Data byte count. + */ + + public final void setAndXByteCount(int off, int cnt) + { + int offset = getAndXByteOffset(off) - 2; + DataPacker.putIntelShort(cnt, m_smbbuf, offset); + } + + /** + * Set the AndX data byte area in the SMB packet + * + * @param off Offset to the AndX command. + * @param byts Byte array containing the data to be copied to the SMB packet. + */ + + public final void setAndXBytes(int off, byte[] byts) + { + int offset = getAndXByteOffset(off) - 2; + DataPacker.putIntelShort(byts.length, m_smbbuf, offset); + + offset += 2; + + for (int idx = 0; idx < byts.length; m_smbbuf[offset + idx] = byts[idx++]) + ; + } + + /** + * Set the secondary SMB command + * + * @param cmd Secondary SMB command code. + */ + + public final void setAndXCommand(int cmd) + { + m_smbbuf[ANDXCOMMAND] = (byte) cmd; + m_smbbuf[ANDXRESERVED] = (byte) 0; + } + + /** + * Set the AndX command for an AndX command block. + * + * @param off Offset to the current AndX command. + * @param cmd Secondary SMB command code. + */ + + public final void setAndXCommand(int off, int cmd) + { + m_smbbuf[off + 1] = (byte) cmd; + m_smbbuf[off + 2] = (byte) 0; + } + + /** + * Set the specified AndX parameter word. + * + * @param off Offset to the AndX command. + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + + public final void setAndXParameter(int off, int idx, int val) + { + int pos = off + (2 * idx) + 1; + DataPacker.putIntelShort(val, m_smbbuf, pos); + } + + /** + * Set the AndX parameter count + * + * @param off Offset to the AndX command. + * @param cnt Parameter word count. + */ + + public final void setAndXParameterCount(int off, int cnt) + { + m_smbbuf[off] = (byte) cnt; + } + + /** + * Set the data byte count for this SMB packet + * + * @param cnt Data byte count. + */ + + public final void setByteCount(int cnt) + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(cnt, m_smbbuf, offset); + } + + /** + * Set the data byte count for this SMB packet + */ + + public final void setByteCount() + { + int offset = getByteOffset() - 2; + int len = m_pos - getByteOffset(); + DataPacker.putIntelShort(len, m_smbbuf, offset); + } + + /** + * Set the data byte area in the SMB packet + * + * @param byts Byte array containing the data to be copied to the SMB packet. + */ + + public final void setBytes(byte[] byts) + { + int offset = getByteOffset() - 2; + DataPacker.putIntelShort(byts.length, m_smbbuf, offset); + + offset += 2; + + for (int idx = 0; idx < byts.length; m_smbbuf[offset + idx] = byts[idx++]) + ; + } + + /** + * Set the SMB command + * + * @param cmd SMB command code + */ + + public final void setCommand(int cmd) + { + m_pkttype = cmd; + m_smbbuf[COMMAND] = (byte) cmd; + } + + /** + * Set the error class and code. + * + * @param errCode int + * @param errClass int + */ + public final void setError(int errCode, int errClass) + { + + // Set the error class and code + + setErrorClass(errClass); + setErrorCode(errCode); + } + + /** + * Set the error class/code. + * + * @param longError boolean + * @param ntErr int + * @param errCode int + * @param errClass int + */ + public final void setError(boolean longError, int ntErr, int errCode, int errClass) + { + + // Check if the error code is a long/NT status code + + if (longError) + { + + // Set the NT status code + + setLongErrorCode(ntErr); + + // Set the NT status code flag + + if (isLongErrorCode() == false) + setFlags2(getFlags2() + SMBSrvPacket.FLG2_LONGERRORCODE); + } + else + { + + // Set the error class and code + + setErrorClass(errClass); + setErrorCode(errCode); + } + } + + /** + * Set the SMB error class. + * + * @param cl SMB error class. + */ + + public final void setErrorClass(int cl) + { + m_smbbuf[ERRORCLASS] = (byte) (cl & 0xFF); + } + + /** + * Set the SMB error code + * + * @param sts SMB error code. + */ + + public final void setErrorCode(int sts) + { + m_smbbuf[ERROR] = (byte) (sts & 0xFF); + } + + /** + * Set the long SMB error code + * + * @param err Long SMB error code. + */ + + public final void setLongErrorCode(int err) + { + DataPacker.putIntelInt(err, m_smbbuf, ERRORCODE); + } + + /** + * Set the SMB flags value. + * + * @param flg SMB flags value. + */ + + public final void setFlags(int flg) + { + m_smbbuf[FLAGS] = (byte) flg; + } + + /** + * Set the SMB flags2 value. + * + * @param flg SMB flags2 value. + */ + + public final void setFlags2(int flg) + { + DataPacker.putIntelShort(flg, m_smbbuf, FLAGS2); + } + + /** + * Set the multiplex identifier. + * + * @param mid Multiplex identifier + */ + + public final void setMultiplexId(int mid) + { + DataPacker.putIntelShort(mid, m_smbbuf, MID); + } + + /** + * Set the specified parameter word. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + + public final void setParameter(int idx, int val) + { + int pos = WORDCNT + (2 * idx) + 1; + DataPacker.putIntelShort(val, m_smbbuf, pos); + } + + /** + * Set the parameter count + * + * @param cnt Parameter word count. + */ + + public final void setParameterCount(int cnt) + { + + // Set the parameter count + + m_smbbuf[WORDCNT] = (byte) cnt; + + // Reset the byte area pointer + + resetBytePointer(); + } + + /** + * Set the specified parameter words. + * + * @param idx Parameter index (zero based). + * @param val Parameter value. + */ + + public final void setParameterLong(int idx, int val) + { + int pos = WORDCNT + (2 * idx) + 1; + DataPacker.putIntelInt(val, m_smbbuf, pos); + } + + /** + * Set the pack/unpack position + * + * @param pos int + */ + public final void setPosition(int pos) + { + m_pos = pos; + } + + /** + * Set the process identifier value (PID). + * + * @param pid Process identifier value. + */ + + public final void setProcessId(int pid) + { + DataPacker.putIntelShort(pid, m_smbbuf, PID); + } + + /** + * Set the actual received data length. + * + * @param len int + */ + public final void setReceivedLength(int len) + { + m_rxLen = len; + } + + /** + * Set the packet sequence number, for connectionless commands. + * + * @param seq Sequence number. + */ + + public final void setSeqNo(int seq) + { + DataPacker.putIntelShort(seq, m_smbbuf, SEQNO); + } + + /** + * Set the session id. + * + * @param sid Session id. + */ + public final void setSID(int sid) + { + DataPacker.putIntelShort(sid, m_smbbuf, SID); + } + + /** + * Set the tree identifier (TID) + * + * @param tid Tree identifier value. + */ + + public final void setTreeId(int tid) + { + DataPacker.putIntelShort(tid, m_smbbuf, TID); + } + + /** + * Set the user identifier (UID) + * + * @param uid User identifier value. + */ + + public final void setUserId(int uid) + { + DataPacker.putIntelShort(uid, m_smbbuf, UID); + } + + /** + * Reset the byte pointer area for packing/unpacking data items from the packet + */ + public final void resetBytePointer() + { + m_pos = getByteOffset(); + m_endpos = m_pos + getByteCount(); + } + + /** + * Set the unpack pointer to the specified offset, for AndX processing + * + * @param off int + * @param len int + */ + public final void setBytePointer(int off, int len) + { + m_pos = off; + m_endpos = m_pos + len; + } + + /** + * Align the byte area pointer on an int (32bit) boundary + */ + public final void alignBytePointer() + { + m_pos = DataPacker.longwordAlign(m_pos); + } + + /** + * Reset the byte/parameter pointer area for packing/unpacking data items from the packet, and + * align the buffer on an int (32bit) boundary + */ + public final void resetBytePointerAlign() + { + m_pos = DataPacker.longwordAlign(getByteOffset()); + m_endpos = m_pos + getByteCount(); + } + + /** + * Skip a number of bytes in the parameter/byte area + * + * @param cnt int + */ + public final void skipBytes(int cnt) + { + m_pos += cnt; + } + + /** + * Set the data buffer + * + * @param buf byte[] + */ + public final void setBuffer(byte[] buf) + { + m_smbbuf = buf; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBSrvSession.java b/source/java/org/alfresco/filesys/smb/server/SMBSrvSession.java new file mode 100644 index 0000000000..967210fe53 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBSrvSession.java @@ -0,0 +1,2063 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import javax.transaction.UserTransaction; + +import org.alfresco.filesys.netbios.NetBIOSException; +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.NetBIOSPacket; +import org.alfresco.filesys.netbios.NetBIOSSession; +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.SrvAuthenticator; +import org.alfresco.filesys.server.core.DeviceInterface; +import org.alfresco.filesys.server.core.SharedDevice; +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.filesys.server.filesys.TooManyConnectionsException; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.smb.Capability; +import org.alfresco.filesys.smb.DataType; +import org.alfresco.filesys.smb.Dialect; +import org.alfresco.filesys.smb.DialectSelector; +import org.alfresco.filesys.smb.NTTime; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.SMBDate; +import org.alfresco.filesys.smb.SMBErrorText; +import org.alfresco.filesys.smb.SMBStatus; +import org.alfresco.filesys.smb.server.notify.NotifyRequest; +import org.alfresco.filesys.smb.server.notify.NotifyRequestList; +import org.alfresco.filesys.util.DataPacker; +import org.alfresco.filesys.util.StringList; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * SMB Session Class + * + *

    + * The SMB server creates a server session object for each incoming session request. + *

    + * The server session holds the context of a particular session, including the list of open files + * and active searches. + */ +public class SMBSrvSession extends SrvSession implements Runnable +{ + // Debug logging + private static Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Define the default receive buffer size to allocate. + + private static final int DefaultBufferSize = 0x010000 + RFCNetBIOSProtocol.HEADER_LEN; + private static final int LanManBufferSize = 8192; + + // Default and maximum number of connection slots + + private static final int DefaultConnections = 4; + private static final int MaxConnections = 16; + + // Tree ids are 16bit values + + private static final int TreeIdMask = 0x0000FFFF; + + // Default and maximum number of search slots + + private static final int DefaultSearches = 8; + private static final int MaxSearches = 256; + + // Maximum multiplexed packets allowed (client can send up to this many SMBs before waiting for + // a response) + // + // Setting NTMaxMultiplexed to one will disable asynchronous notifications on the client + + private static final int LanManMaxMultiplexed = 1; + private static final int NTMaxMultiplexed = 4; + + // Maximum number of virtual circuits + + private static final int MaxVirtualCircuits = 0; + + // Packet handler used to send/receive SMB packets over a particular protocol + + private PacketHandler m_pktHandler; + + // Packet buffer for received data and received data length. + + private byte[] m_buf; + private int m_rxlen; + + // SMB packet used for response + + private SMBSrvPacket m_smbPkt; + + // Protocol handler for this session, depends upon the negotiated SMB dialect + + private ProtocolHandler m_handler; + + // SMB session state. + + private int m_state = SMBSrvSessionState.NBSESSREQ; + + // SMB dialect that this session has negotiated to use. + + private int m_dialect = Dialect.Unknown; + + // Callers NetBIOS name and target name + + private String m_callerNBName; + private String m_targetNBName; + + // Connected share list and next tree id + + private Hashtable m_connections; + private int m_treeId; + + // Active search list for this session + + private SearchContext[] m_search; + private int m_searchCount; + + // Active transaction details + + private SrvTransactBuffer m_transact; + + // Notify change requests and notifications pending flag + + private NotifyRequestList m_notifyList; + private boolean m_notifyPending; + + // Default SMB/CIFS flags anf flags2, ORed with the SMB packet flags/flags2 before sending a + // response to the client. + + private int m_defFlags; + private int m_defFlags2; + + // Asynchrnous response packet queue + // + // Contains SMB response packets that could not be sent due to SMB requests being processed. The + // asynchronous responses must be sent after any pending requests have been processed as the client may + // disconnect the session. + + private Vector m_asynchQueue; + + // Maximum client buffer size and multiplex count + + private int m_maxBufSize; + private int m_maxMultiplex; + + // Client capabilities + + private int m_clientCaps; + + // Debug flag values + + public static final int DBG_NETBIOS = 0x00000001; // NetBIOS layer + public static final int DBG_STATE = 0x00000002; // Session state changes + public static final int DBG_NEGOTIATE = 0x00000004; // Protocol negotiate phase + public static final int DBG_TREE = 0x00000008; // Tree connection/disconnection + public static final int DBG_SEARCH = 0x00000010; // File/directory search + public static final int DBG_INFO = 0x00000020; // Information requests + public static final int DBG_FILE = 0x00000040; // File open/close/info + public static final int DBG_FILEIO = 0x00000080; // File read/write + public static final int DBG_TRAN = 0x00000100; // Transactions + public static final int DBG_ECHO = 0x00000200; // Echo requests + public static final int DBG_ERROR = 0x00000400; // Errors + public static final int DBG_IPC = 0x00000800; // IPC$ requests + public static final int DBG_LOCK = 0x00001000; // Lock/unlock requests + public static final int DBG_PKTTYPE = 0x00002000; // Received packet type + public static final int DBG_DCERPC = 0x00004000; // DCE/RPC + public static final int DBG_STATECACHE = 0x00008000; // File state cache + public static final int DBG_NOTIFY = 0x00010000; // Asynchronous change notification + public static final int DBG_STREAMS = 0x00020000; // NTFS streams + public static final int DBG_SOCKET = 0x00040000; // NetBIOS/native SMB socket connections + + /** + * Class constructor. + * + * @param handler Packet handler used to send/receive SMBs + * @param srv Server that this session is associated with. + */ + public SMBSrvSession(PacketHandler handler, SMBServer srv) + { + super(-1, srv, handler.isProtocolName(), null); + + // Set the packet handler + + m_pktHandler = handler; + + // Allocate a receive buffer + + m_buf = new byte[DefaultBufferSize]; + m_smbPkt = new SMBSrvPacket(m_buf); + + // If this is a TCPIP SMB or Win32 NetBIOS session then bypass the NetBIOS session setup + // phase. + + if (isProtocol() == SMBSrvPacket.PROTOCOL_TCPIP || isProtocol() == SMBSrvPacket.PROTOCOL_WIN32NETBIOS) + { + + // Advance to the SMB negotiate dialect phase + + setState(SMBSrvSessionState.SMBNEGOTIATE); + + // Check if the client name is available + + if (handler.hasClientName()) + m_callerNBName = handler.getClientName(); + } + } + + /** + * Return the session protocol type + * + * @return int + */ + public final int isProtocol() + { + return m_pktHandler.isProtocol(); + } + + /** + * Add a new connection to this session. Return the allocated tree id for the new connection. + * + * @return int Allocated tree id (connection id). + * @param shrDev SharedDevice + */ + protected int addConnection(SharedDevice shrDev) throws TooManyConnectionsException + { + + // Check if the connection array has been allocated + + if (m_connections == null) + m_connections = new Hashtable(DefaultConnections); + + // Allocate an id for the tree connection + + int treeId = 0; + + synchronized (m_connections) + { + + // Check if the tree connection table is full + + if (m_connections.size() == MaxConnections) + throw new TooManyConnectionsException(); + + // Find a free slot in the connection array + + treeId = (m_treeId++ & TreeIdMask); + Integer key = new Integer(treeId); + + while (m_connections.contains(key)) + { + + // Try another tree id for the new connection + + treeId = (m_treeId++ & TreeIdMask); + key = new Integer(treeId); + } + + // Store the new tree connection + + m_connections.put(key, new TreeConnection(shrDev)); + } + + // Return the allocated tree id + + return treeId; + } + + /** + * Allocate a slot in the active searches list for a new search. + * + * @return int Search slot index, or -1 if there are no more search slots available. + */ + protected final int allocateSearchSlot() + { + + // Check if the search array has been allocated + + if (m_search == null) + m_search = new SearchContext[DefaultSearches]; + + // Find a free slot for the new search + + int idx = 0; + + while (idx < m_search.length && m_search[idx] != null) + idx++; + + // Check if we found a free slot + + if (idx == m_search.length) + { + + // The search array needs to be extended, check if we reached the limit. + + if (m_search.length >= MaxSearches) + return -1; + + // Extend the search array + + SearchContext[] newSearch = new SearchContext[m_search.length * 2]; + System.arraycopy(m_search, 0, newSearch, 0, m_search.length); + m_search = newSearch; + } + + // Return the allocated search slot index + + m_searchCount++; + return idx; + } + + /** + * Cleanup any resources owned by this session, close files, searches and change notification + * requests. + */ + protected final void cleanupSession() + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("Cleanup session, searches=" + getSearchCount() + ", treeConns=" + getConnectionCount() + + ", changeNotify=" + getNotifyChangeCount()); + + // Check if there are any active searches + + if (m_search != null) + { + + // Close all active searches + + for (int idx = 0; idx < m_search.length; idx++) + { + + // Check if the current search slot is active + + if (m_search[idx] != null) + deallocateSearchSlot(idx); + } + + // Release the search context list, clear the search count + + m_search = null; + m_searchCount = 0; + } + + // Check if there are open tree connections + + if (m_connections != null) + { + + synchronized (m_connections) + { + + // Close all active tree connections + + Enumeration enm = m_connections.elements(); + + while (enm.hasMoreElements()) + { + + // Get the current tree connection + + TreeConnection tree = enm.nextElement(); + DeviceInterface devIface = tree.getInterface(); + + // Check if there are open files on the share + + if (tree.openFileCount() > 0) + { + + // Close the open files, release locks + + for (int i = 0; i < tree.getFileTableLength(); i++) + { + + // Get an open file + + NetworkFile curFile = tree.findFile(i); + if (curFile != null && devIface instanceof DiskInterface) + { + + // Access the disk share interface + + DiskInterface diskIface = (DiskInterface) devIface; + + try + { + + // Remove the file from the tree connection list + + tree.removeFile(i, this); + + // Close the file + + diskIface.closeFile(this, tree, curFile); + } + catch (Exception ex) + { + } + } + } + } + // Inform the driver that the connection has been closed + + if (devIface != null) + devIface.treeClosed(this, tree); + } + + // Clear the tree connection list + + m_connections.clear(); + } + } + + // Check if there are active change notification requests + + if (m_notifyList != null && m_notifyList.numberOfRequests() > 0) + { + + // Remove the notify requests from the associated device context notify list + + for (int i = 0; i < m_notifyList.numberOfRequests(); i++) + { + + // Get the current change notification request and remove from the global notify + // list + + NotifyRequest curReq = m_notifyList.getRequest(i); + curReq.getDiskContext().getChangeHandler().removeNotifyRequests(this); + } + } + + // Delete any temporary shares that were created for this session + + getSMBServer().deleteTemporaryShares(this); + } + + /** + * Close the session socket + */ + protected final void closeSocket() + { + + // Indicate that the session is being shutdown + + setShutdown(true); + + // Close the packet handler + + try + { + m_pktHandler.closeHandler(); + } + catch (Exception ex) + { + } + } + + /** + * Close the session + */ + public final void closeSession() + { + + // Call the base class + + super.closeSession(); + + try + { + + // Set the session into a hangup state and indicate that we have shutdown the session + + setState(SMBSrvSessionState.NBHANGUP); + setShutdown(true); + + // Close the packet handler + + m_pktHandler.closeHandler(); + } + catch (Exception ex) + { + } + + } + + /** + * Deallocate the specified search context/slot. + * + * @param ctxId int + */ + protected final void deallocateSearchSlot(int ctxId) + { + + // Check if the search array has been allocated and that the index is valid + + if (m_search == null || ctxId >= m_search.length) + return; + + // Close the search + + if (m_search[ctxId] != null) + m_search[ctxId].closeSearch(); + + // Free the specified search context slot + + m_searchCount--; + m_search[ctxId] = null; + } + + /** + * Finalize, object is about to be garbage collected. Make sure resources are released. + */ + public void finalize() + { + + // Check if there are any active resources + + cleanupSession(); + + // Make sure the socket is closed and deallocated + + closeSocket(); + } + + /** + * Return the tree connection details for the specified tree id. + * + * @param treeId int + * @return TreeConnection + */ + protected final TreeConnection findConnection(int treeId) + { + + // Check if the tree id and connection array are valid + + if (m_connections == null) + return null; + + // Get the required tree connection details + + return (TreeConnection) m_connections.get(new Integer(treeId)); + } + + /** + * Return the input/output metwork buffer for this session. + * + * @return byte[] + */ + protected final byte[] getBuffer() + { + return m_buf; + } + + /** + * Return the count of active connections for this session. + * + * @return int + */ + public final int getConnectionCount() + { + return m_connections != null ? m_connections.size() : 0; + } + + /** + * Return the default flags SMB header value + * + * @return int + */ + public final int getDefaultFlags() + { + return m_defFlags; + } + + /** + * Return the default flags2 SMB header value + * + * @return int + */ + public final int getDefaultFlags2() + { + return m_defFlags2; + } + + /** + * Return the count of active change notification requests + * + * @return int + */ + public final int getNotifyChangeCount() + { + if (m_notifyList == null) + return 0; + return m_notifyList.numberOfRequests(); + } + + /** + * Return the client maximum buffer size + * + * @return int + */ + public final int getClientMaximumBufferSize() + { + return m_maxBufSize; + } + + /** + * Return the client maximum muliplexed requests + * + * @return int + */ + public final int getClientMaximumMultiplex() + { + return m_maxMultiplex; + } + + /** + * Return the client capability flags + * + * @return int + */ + public final int getClientCapabilities() + { + return m_clientCaps; + } + + /** + * Determine if the client has the specified capability enabled + * + * @param cap int + * @return boolean + */ + public final boolean hasClientCapability(int cap) + { + if ((m_clientCaps & cap) != 0) + return true; + return false; + } + + /** + * Return the SMB dialect type that the server/client have negotiated. + * + * @return int + */ + public final int getNegotiatedSMBDialect() + { + return m_dialect; + } + + /** + * Return the packet handler used by the session + * + * @return PacketHandler + */ + public final PacketHandler getPacketHandler() + { + return m_pktHandler; + } + + /** + * Return the receiver SMB packet. + * + * @return SMBSrvPacket + */ + public final SMBSrvPacket getReceivePacket() + { + return m_smbPkt; + } + + /** + * Return the remote NetBIOS name that was used to create the session. + * + * @return java.lang.String + */ + public final String getRemoteNetBIOSName() + { + return m_callerNBName; + } + + /** + * Check if the session has a target NetBIOS name + * + * @return boolean + */ + public final boolean hasTargetNetBIOSName() + { + return m_targetNBName != null ? true : false; + } + + /** + * Return the target NetBIOS name that was used to create the session + * + * @return String + */ + public final String getTargetNetBIOSName() + { + return m_targetNBName; + } + + /** + * Cehck if the clients remote address is available + * + * @return boolean + */ + public final boolean hasRemoteAddress() + { + return m_pktHandler.hasRemoteAddress(); + } + + /** + * Return the client network address + * + * @return InetAddress + */ + public final InetAddress getRemoteAddress() + { + return m_pktHandler.getRemoteAddress(); + } + + /** + * Return the search context for the specified search id. + * + * @param srchId int + * @return SearchContext + */ + protected final SearchContext getSearchContext(int srchId) + { + + // Check if the search array is valid and the search index is valid + + if (m_search == null || srchId >= m_search.length) + return null; + + // Return the required search context + + return m_search[srchId]; + } + + /** + * Return the number of active tree searches. + * + * @return int + */ + public final int getSearchCount() + { + return m_searchCount; + } + + /** + * Return the server that this session is associated with. + * + * @return SMBServer + */ + public final SMBServer getSMBServer() + { + return (SMBServer) getServer(); + } + + /** + * Return the server name that this session is associated with. + * + * @return java.lang.String + */ + public final String getServerName() + { + return getSMBServer().getServerName(); + } + + /** + * Hangup the session. + * + * @param reason java.lang.String Reason the session is being closed. + */ + private void hangupSession(String reason) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_NETBIOS)) + logger.debug("## Session closing - " + reason); + + // Set the session into a NetBIOS hangup state + + setState(SMBSrvSessionState.NBHANGUP); + } + + /** + * Check if the Macintosh exteniosn SMBs are enabled + * + * @return boolean + */ + public final boolean hasMacintoshExtensions() + { + return getSMBServer().getConfiguration().hasMacintoshExtensions(); + } + + /** + * Check if there is a change notification update pending + * + * @return boolean + */ + public final boolean hasNotifyPending() + { + return m_notifyPending; + } + + /** + * Set the change notify pending flag + * + * @param pend boolean + */ + public final void setNotifyPending(boolean pend) + { + m_notifyPending = pend; + } + + /** + * Set the client maximum buffer size + * + * @param maxBuf int + */ + public final void setClientMaximumBufferSize(int maxBuf) + { + m_maxBufSize = maxBuf; + } + + /** + * Set the client maximum multiplexed + * + * @param maxMpx int + */ + public final void setClientMaximumMultiplex(int maxMpx) + { + m_maxMultiplex = maxMpx; + } + + /** + * Set the client capability flags + * + * @param flags int + */ + public final void setClientCapabilities(int flags) + { + m_clientCaps = flags; + } + + /** + * Set the default flags value to be ORed with outgoing response packet flags + * + * @param flags int + */ + public final void setDefaultFlags(int flags) + { + m_defFlags = flags; + } + + /** + * Set the default flags2 value to be ORed with outgoing response packet flags2 field + * + * @param flags int + */ + public final void setDefaultFlags2(int flags) + { + m_defFlags2 = flags; + } + + /** + * Set the SMB packet + * + * @param pkt SMBSrvPacket + */ + public final void setReceivePacket(SMBSrvPacket pkt) + { + m_smbPkt = pkt; + m_buf = pkt.getBuffer(); + } + + /** + * Store the seach context in the specified slot. + * + * @param slot Slot to store the search context. + * @param srch SearchContext + */ + protected final void setSearchContext(int slot, SearchContext srch) + { + + // Check if the search slot id is valid + + if (m_search == null || slot > m_search.length) + return; + + // Store the context + + m_search[slot] = srch; + } + + /** + * Set the session state. + * + * @param state int + */ + protected void setState(int state) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("State changed to " + SMBSrvSessionState.getStateAsString(state)); + + // Change the session state + + m_state = state; + } + + /** + * Process the NetBIOS session request message, either accept the session request and send back + * a NetBIOS accept or reject the session and send back a NetBIOS reject and hangup the session. + */ + protected void procNetBIOSSessionRequest() throws IOException, NetBIOSException + { + + // Check if the received packet contains enough data for a NetBIOS session request packet. + + NetBIOSPacket nbPkt = new NetBIOSPacket(m_buf); + + if (m_rxlen < RFCNetBIOSProtocol.SESSREQ_LEN || nbPkt.getHeaderType() != RFCNetBIOSProtocol.SESSION_REQUEST) + throw new NetBIOSException("NBREQ Invalid packet"); + + // Do a few sanity checks on the received packet + + if (m_buf[4] != (byte) 32 || m_buf[38] != (byte) 32) + throw new NetBIOSException("NBREQ Invalid NetBIOS name data"); + + // Extract the from/to NetBIOS encoded names, and convert to normal strings. + + StringBuffer nbName = new StringBuffer(32); + for (int i = 0; i < 32; i++) + nbName.append((char) m_buf[5 + i]); + String toName = NetBIOSSession.DecodeName(nbName.toString()); + toName = toName.trim(); + + nbName.setLength(0); + for (int i = 0; i < 32; i++) + nbName.append((char) m_buf[39 + i]); + String fromName = NetBIOSSession.DecodeName(nbName.toString()); + fromName = fromName.trim(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_NETBIOS)) + logger.debug("NetBIOS CALL From " + fromName + " to " + toName); + + // Check that the request is for this server + + boolean forThisServer = false; + + if (toName.compareTo(getServerName()) == 0 || toName.compareTo(NetBIOSName.SMBServer) == 0 + || toName.compareTo(NetBIOSName.SMBServer2) == 0 || toName.compareTo("*") == 0) + { + + // Request is for this server + + forThisServer = true; + } + else + { + + // Check if the caller is using an IP address + + InetAddress[] srvAddr = getSMBServer().getServerAddresses(); + if (srvAddr != null) + { + + // Check for an address match + + int idx = 0; + + while (idx < srvAddr.length && forThisServer == false) + { + + // Check the current IP address + + if (srvAddr[idx++].getHostAddress().compareTo(toName) == 0) + forThisServer = true; + } + } + } + + // If we did not find an address match then reject the session request + + if (forThisServer == false) + throw new NetBIOSException("NBREQ Called name is not this server (" + toName + ")"); + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_NETBIOS)) + logger.debug("NetBIOS session request from " + fromName); + + // Save the callers name and target name + + m_callerNBName = fromName; + m_targetNBName = toName; + + // Set the remote client name + + setRemoteName(fromName); + + // Build a NetBIOS session accept message + + nbPkt.setHeaderType(RFCNetBIOSProtocol.SESSION_ACK); + nbPkt.setHeaderFlags(0); + nbPkt.setHeaderLength(0); + + // Output the NetBIOS session accept packet + + m_pktHandler.writePacket(m_buf, 0, 4); + + // Move the session to the SMB negotiate state + + setState(SMBSrvSessionState.SMBNEGOTIATE); + } + + /** + * Process an SMB dialect negotiate request. + */ + protected void procSMBNegotiate() throws SMBSrvException, IOException + { + + // Create an SMB server packet using the receive buffer + + m_smbPkt = new SMBSrvPacket(m_buf); + + // Initialize the NetBIOS header + + m_buf[0] = (byte) RFCNetBIOSProtocol.SESSION_MESSAGE; + + // Check if the received packet looks like a valid SMB + + if (m_smbPkt.getCommand() != PacketType.Negotiate || m_smbPkt.checkPacketIsValid(0, 2) == false) + { + sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv); + return; + } + + // Decode the data block into a list of requested SMB dialects + + int dataPos = m_smbPkt.getByteOffset(); + int dataLen = m_smbPkt.getByteCount(); + + String diaStr = null; + StringList dialects = new StringList(); + + while (dataLen > 0) + { + + // Decode an SMB dialect string from the data block, always ASCII strings + + diaStr = DataPacker.getDataString(DataType.Dialect, m_buf, dataPos, dataLen, false); + if (diaStr != null) + { + + // Add the dialect string to the list of requested dialects + + dialects.addString(diaStr); + } + else + { + + // Invalid dialect block in the negotiate packet, send an error response and hangup + // the session. + + sendErrorResponseSMB(SMBStatus.SRVNonSpecificError, SMBStatus.ErrSrv); + setState(SMBSrvSessionState.NBHANGUP); + return; + } + + // Update the remaining data position and count + + dataPos += diaStr.length() + 2; // data type and null + dataLen -= diaStr.length() + 2; + } + + // Find the highest level SMB dialect that the server and client both support + + DialectSelector dia = getSMBServer().getSMBDialects(); + int diaIdx = -1; + + for (int i = 0; i < Dialect.Max; i++) + { + + // Check if the current dialect is supported by the server + + if (dia.hasDialect(i)) + { + + // Check if the client supports the current dialect. If the current dialect is a + // higher level dialect than the currently nominated dialect, update the nominated + // dialect index. + + for (int j = 0; j < Dialect.SMB_PROT_MAXSTRING; j++) + { + + // Check if the dialect string maps to the current dialect index + + if (Dialect.DialectType(j) == i && dialects.containsString(Dialect.DialectString(j))) + { + + // Update the selected dialect type, if the current dialect is a newer + // dialect + + if (i > diaIdx) + diaIdx = i; + } + } + } + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_NEGOTIATE)) + { + if (diaIdx == -1) + logger.debug("Failed to negotiate SMB dialect"); + else + logger.debug("Negotiated SMB dialect - " + Dialect.DialectTypeString(diaIdx)); + } + + // Check if we successfully negotiated an SMB dialect with the client + + if (diaIdx != -1) + { + + // Store the negotiated SMB diialect type + + m_dialect = diaIdx; + + // Convert the dialect type to an index within the clients SMB dialect list + + diaIdx = dialects.findString(Dialect.DialectTypeString(diaIdx)); + + // Allocate a protocol handler for the negotiated dialect, if we cannot get a protocol + // handler then bounce the request. + + m_handler = ProtocolFactory.getHandler(m_dialect); + if (m_handler != null) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_NEGOTIATE)) + logger.debug("Assigned protocol handler - " + m_handler.getClass().getName()); + + // Set the protocol handlers associated session + + m_handler.setSession(this); + } + else + { + + // Could not get a protocol handler for the selected SMB dialect, indicate to the + // client + // that no suitable dialect available. + + diaIdx = -1; + } + } + + // Build the negotiate response SMB for Core dialect + + if (m_dialect == -1 || m_dialect <= Dialect.CorePlus) + { + + // Core dialect negotiate response, or no valid dialect response + + m_smbPkt.setParameterCount(1); + m_smbPkt.setParameter(0, diaIdx); + m_smbPkt.setByteCount(0); + + m_smbPkt.setTreeId(0); + m_smbPkt.setUserId(0); + } + else if (m_dialect <= Dialect.LanMan2_1) + { + + // We are using case sensitive pathnames and long file names + + m_smbPkt.setFlags(SMBSrvPacket.FLG_CASELESS); + m_smbPkt.setFlags2(SMBSrvPacket.FLG2_LONGFILENAMES); + + // Access the authenticator for this server and determine if the server is in share or + // user level + // security mode. + + SrvAuthenticator auth = getServer().getConfiguration().getAuthenticator(); + int secMode = 0; + + if (auth != null) + { + + // Check if the server is in share or user level security mode + + if (auth.getAccessMode() == SrvAuthenticator.USER_MODE) + secMode = 1; + + // Check if encrypted passwords should be used by the client + + if (auth.hasEncryptPasswords()) + secMode += 2; + } + + // LanMan dialect negotiate response + + m_smbPkt.setParameterCount(13); + m_smbPkt.setParameter(0, diaIdx); + m_smbPkt.setParameter(1, secMode); // Security mode, encrypt passwords + m_smbPkt.setParameter(2, LanManBufferSize); + m_smbPkt.setParameter(3, LanManMaxMultiplexed); // maximum multiplexed requests + m_smbPkt.setParameter(4, MaxVirtualCircuits); // maximum number of virtual circuits + m_smbPkt.setParameter(5, 0); // read/write raw mode support + + // Create a session token, using the system clock + + m_smbPkt.setParameterLong(6, (int) (System.currentTimeMillis() & 0xFFFFFFFF)); + + // Return the current server date/time + + SMBDate srvDate = new SMBDate(System.currentTimeMillis()); + m_smbPkt.setParameter(8, srvDate.asSMBTime()); + m_smbPkt.setParameter(9, srvDate.asSMBDate()); + + // Server timezone offset from UTC + + m_smbPkt.setParameter(10, getServer().getConfiguration().getTimeZoneOffset()); + + // Encryption key length + + m_smbPkt.setParameter(11, 8); // Encryption key length + m_smbPkt.setParameter(12, 0); + + // Encryption key and primary domain string should be returned in the byte area + + setChallengeKey(auth.getChallengeKey(this)); + int pos = m_smbPkt.getByteOffset(); + byte[] buf = m_smbPkt.getBuffer(); + + if (hasChallengeKey() == false) + { + + // Return a dummy encryption key + + for (int i = 0; i < 8; i++) + buf[pos++] = 0; + } + else + { + + // Store the encryption key + + byte[] key = getChallengeKey(); + for (int i = 0; i < key.length; i++) + buf[pos++] = key[i]; + } + + // Set the local domain name + + String domain = getServer().getConfiguration().getDomainName(); + if (domain != null) + pos = DataPacker.putString(domain, buf, pos, true); + + m_smbPkt.setByteCount(pos - m_smbPkt.getByteOffset()); + + m_smbPkt.setTreeId(0); + m_smbPkt.setUserId(0); + } + else if (m_dialect == Dialect.NT) + { + + // We are using case sensitive pathnames and long file names + + setDefaultFlags(SMBSrvPacket.FLG_CASELESS); + setDefaultFlags2(SMBSrvPacket.FLG2_LONGFILENAMES + SMBSrvPacket.FLG2_UNICODE); + + // Access the authenticator for this server and determine if the server is in share or + // user level + // security mode. + + SrvAuthenticator auth = getServer().getConfiguration().getAuthenticator(); + int secMode = 0; + + if (auth != null) + { + + // Check if the server is in share or user level security mode + + if (auth.getAccessMode() == SrvAuthenticator.USER_MODE) + secMode = 1; + + // Check if encrypted passwords should be used by the client + + if (auth.hasEncryptPasswords()) + secMode += 2; + } + + // NT dialect negotiate response + + NTParameterPacker nt = new NTParameterPacker(m_smbPkt.getBuffer()); + + m_smbPkt.setParameterCount(17); + nt.packWord(diaIdx); // selected dialect index + nt.packByte(secMode); // security mode + nt.packWord(NTMaxMultiplexed); // maximum multiplexed requests + // setting to 1 will disable change notify requests from the client + nt.packWord(MaxVirtualCircuits); // maximum number of virtual circuits + + int maxBufSize = m_smbPkt.getBuffer().length - RFCNetBIOSProtocol.HEADER_LEN; + nt.packInt(maxBufSize); + + nt.packInt(0); // maximum raw size + + // Create a session token, using the system clock + + nt.packInt((int) (System.currentTimeMillis() & 0xFFFFFFFFL)); + + // Set server capabilities + + nt.packInt(Capability.Unicode + Capability.RemoteAPIs + Capability.NTSMBs + Capability.NTFind + + Capability.NTStatus + Capability.LargeFiles + Capability.LargeRead + Capability.LargeWrite); + + // Return the current server date/time, and timezone + + long srvTime = NTTime.toNTTime(new java.util.Date(System.currentTimeMillis())); + + nt.packLong(srvTime); + nt.packWord(getServer().getConfiguration().getTimeZoneOffset()); + // server timezone offset + + // Encryption key length + + nt.packByte(8); // encryption key length + + // Encryption key and primary domain string should be returned in the byte area + + setChallengeKey(auth.getChallengeKey(this)); + + int pos = m_smbPkt.getByteOffset(); + byte[] buf = m_smbPkt.getBuffer(); + + if (hasChallengeKey() == false) + { + + // Return a dummy encryption key + + for (int i = 0; i < 8; i++) + buf[pos++] = 0; + } + else + { + + // Store the encryption key + + byte[] key = getChallengeKey(); + + for (int i = 0; i < key.length; i++) + buf[pos++] = key[i]; + } + + // Pack the local domain name + + String domain = getServer().getConfiguration().getDomainName(); + if (domain != null) + pos = DataPacker.putUnicodeString(domain, buf, pos, true); + + // Pack the server name + + pos = DataPacker.putUnicodeString(getServerName(), buf, pos, true); + + // Set the packet length + + m_smbPkt.setByteCount(pos - m_smbPkt.getByteOffset()); + + m_smbPkt.setTreeId(0); + m_smbPkt.setUserId(0); + } + + // Make sure the response flag is set + + if (m_smbPkt.isResponse() == false) + m_smbPkt.setFlags(m_smbPkt.getFlags() + SMBPacket.FLG_RESPONSE); + + // Send the negotiate response + + m_pktHandler.writePacket(m_smbPkt, m_smbPkt.getLength()); + + // Check if the negotiated SMB dialect supports the session setup command, if not then + // bypass + // the session setup phase. + + if (m_dialect == -1) + setState(SMBSrvSessionState.NBHANGUP); + else if (Dialect.DialectSupportsCommand(m_dialect, PacketType.SessionSetupAndX)) + setState(SMBSrvSessionState.SMBSESSSETUP); + else + setState(SMBSrvSessionState.SMBSESSION); + + // If a dialect was selected inform the server that the session has been opened + + if (m_dialect != -1) + getSMBServer().sessionOpened(this); + } + + /** + * Remove the specified tree connection from the active connection list. + * + * @param treeId int + */ + protected void removeConnection(int treeId) + { + + // Check if the tree id is valid + + if (m_connections == null) + return; + + // Close the connection and remove from the connection list + + synchronized (m_connections) + { + + // Get the connection + + Integer key = new Integer(treeId); + TreeConnection tree = (TreeConnection) m_connections.get(key); + + // Close the connection, release resources + + if (tree != null) + { + + // Close the connection + + tree.closeConnection(this); + + // Remove the connection from the connection list + + m_connections.remove(key); + } + } + } + + /** + * Start the SMB server session in a seperate thread. + */ + public void run() + { + try + { + // Debug + + if (logger.isDebugEnabled() && hasDebug(SMBSrvSession.DBG_NEGOTIATE)) + logger.debug("Server session started"); + + // The server session loops until the NetBIOS hangup state is set. + + while (m_state != SMBSrvSessionState.NBHANGUP) + { + + // Set the current receive length to -1 to indicate that the session thread is not + // currently processing an SMB packet. This is used by the asynchronous response code + // to determine when it can send the response. + + m_rxlen = -1; + + // Wait for a data packet + + m_rxlen = m_pktHandler.readPacket(m_smbPkt); + + // Check for an empty packet + + if (m_rxlen == 0) + continue; + + // Check if there is no more data, the other side has dropped the connection + + if (m_rxlen == -1) + { + hangupSession("Remote disconnect"); + continue; + } + + // Store the received data length + + m_smbPkt.setReceivedLength(m_rxlen); + + // Update the request count + + m_reqCount++; + + // Process the received packet + + switch (m_state) + { + + // NetBIOS session request pending + + case SMBSrvSessionState.NBSESSREQ: + procNetBIOSSessionRequest(); + break; + + // SMB dialect negotiate + + case SMBSrvSessionState.SMBNEGOTIATE: + procSMBNegotiate(); + break; + + // SMB session setup + + case SMBSrvSessionState.SMBSESSSETUP: + m_handler.runProtocol(); + break; + + // SMB session main request processing + + case SMBSrvSessionState.SMBSESSION: + + // Run the main protocol handler + + runHandler(); + break; + + } // end switch session state + + // Check for an active transaction, and commit it + + if ( hasUserTransaction()) + { + try + { + // Commit the transaction + + UserTransaction trans = getUserTransaction(); + trans.commit(); + } + catch ( Exception ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Error committing transaction", ex); + } + } + + // Give up the CPU + + Thread.yield(); + + } // end while state + } + catch (SocketException ex) + { + + // DEBUG + + logger.error("Socket closed by remote client"); + } + catch (Exception ex) + { + + // Output the exception details + + if (isShutdown() == false) + logger.error("Closing session due to exception", ex); + } + catch (Throwable ex) + { + logger.error("Closing session due to throwable", ex); + } + finally + { + // If there is an active transaction then roll it back + + if ( hasUserTransaction()) + { + try + { + getUserTransaction().rollback(); + } + catch (Exception ex) + { + logger.warn("Failed to rollback transaction", ex); + } + } + } + + // Cleanup the session, make sure all resources are released + + cleanupSession(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_STATE)) + logger.debug("Server session closed"); + + // Close the session + + closeSocket(); + + // Notify the server that the session has closed + + getSMBServer().sessionClosed(this); + } + + /** + * Handle a session message, receive all data and run the SMB protocol handler. + */ + protected final void runHandler() throws IOException, SMBSrvException, TooManyConnectionsException + { + + // Make sure we received at least a NetBIOS header + + if (m_rxlen < NetBIOSPacket.MIN_RXLEN) + return; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_PKTTYPE)) + logger.debug("Rx packet type - " + m_smbPkt.getPacketTypeString() + ", SID=" + m_smbPkt.getSID()); + + // Call the protocol handler + + if (m_handler.runProtocol() == false) + { + + // The sessions protocol handler did not process the request, return an unsupported + // SMB error status. + + sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv); + } + + // Check if there are any pending asynchronous response packets + + while (hasAsynchResponse()) + { + + // Remove the current asynchronous response SMB packet and send to the client + + SMBSrvPacket asynchPkt = removeFirstAsynchResponse(); + sendResponseSMB(asynchPkt, asynchPkt.getLength()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_NOTIFY)) + logger.debug("Sent queued asynch response type=" + asynchPkt.getPacketTypeString() + ", mid=" + + asynchPkt.getMultiplexId() + ", pid=" + asynchPkt.getProcessId()); + } + } + + /** + * Send an SMB response + * + * @param pkt SMBSrvPacket + * @exception IOException + */ + public final void sendResponseSMB(SMBSrvPacket pkt) throws IOException + { + sendResponseSMB(pkt, pkt.getLength()); + } + + /** + * Send an SMB response + * + * @param pkt SMBSrvPacket + * @param len int + * @exception IOException + */ + public synchronized final void sendResponseSMB(SMBSrvPacket pkt, int len) throws IOException + { + + // Make sure the response flag is set + + if (pkt.isResponse() == false) + pkt.setFlags(pkt.getFlags() + SMBSrvPacket.FLG_RESPONSE); + + // Add default flags/flags2 values + + pkt.setFlags(pkt.getFlags() | getDefaultFlags()); + + // Mask out certain flags that the client may have sent + + int flags2 = pkt.getFlags2() | getDefaultFlags2(); + flags2 &= ~(SMBPacket.FLG2_EXTENDEDATTRIB + SMBPacket.FLG2_EXTENDNEGOTIATE + SMBPacket.FLG2_DFSRESOLVE + SMBPacket.FLG2_SECURITYSIGS); + + pkt.setFlags2(flags2); + + // Send the response packet + + m_pktHandler.writePacket(pkt, len); + m_pktHandler.flushPacket(); + } + + /** + * Send a success response SMB + * + * @exception IOException If a network error occurs + */ + public final void sendSuccessResponseSMB() throws IOException + { + + // Make sure the response flag is set + + if (m_smbPkt.isResponse() == false) + m_smbPkt.setFlags(m_smbPkt.getFlags() + SMBSrvPacket.FLG_RESPONSE); + + // Add default flags/flags2 values + + m_smbPkt.setFlags(m_smbPkt.getFlags() | getDefaultFlags()); + m_smbPkt.setFlags2(m_smbPkt.getFlags2() | getDefaultFlags2()); + + // Clear the parameter and byte counts + + m_smbPkt.setParameterCount(0); + m_smbPkt.setByteCount(0); + + if (m_smbPkt.isLongErrorCode()) + m_smbPkt.setLongErrorCode(SMBStatus.NTSuccess); + else + { + m_smbPkt.setErrorClass(SMBStatus.Success); + m_smbPkt.setErrorCode(SMBStatus.Success); + } + + // Return the success response to the client + + sendResponseSMB(m_smbPkt, m_smbPkt.getLength()); + } + + /** + * Send an error response SMB. The returned code depends on the client long error code flag + * setting. + * + * @param ntCode 32bit error code + * @param stdCode Standard error code + * @param StdClass Standard error class + */ + public final void sendErrorResponseSMB(int ntCode, int stdCode, int stdClass) throws java.io.IOException + { + + // Check if long error codes are required by the client + + if (m_smbPkt.isLongErrorCode()) + { + + // Return the long/NT status code + + sendErrorResponseSMB(ntCode, SMBStatus.NTErr); + } + else + { + + // Return the standard/DOS error code + + sendErrorResponseSMB(stdCode, stdClass); + } + } + + /** + * Send an error response SMB. + * + * @param errCode int Error code. + * @param errClass int Error class. + */ + public final void sendErrorResponseSMB(int errCode, int errClass) throws java.io.IOException + { + + // Make sure the response flag is set + + if (m_smbPkt.isResponse() == false) + m_smbPkt.setFlags(m_smbPkt.getFlags() + SMBSrvPacket.FLG_RESPONSE); + + // Set the error code and error class in the response packet + + m_smbPkt.setParameterCount(0); + m_smbPkt.setByteCount(0); + + // Add default flags/flags2 values + + m_smbPkt.setFlags(m_smbPkt.getFlags() | getDefaultFlags()); + m_smbPkt.setFlags2(m_smbPkt.getFlags2() | getDefaultFlags2()); + + // Check if the error is a NT 32bit error status + + if (errClass == SMBStatus.NTErr) + { + + // Enable the long error status flag + + if (m_smbPkt.isLongErrorCode() == false) + m_smbPkt.setFlags2(m_smbPkt.getFlags2() + SMBSrvPacket.FLG2_LONGERRORCODE); + + // Set the NT status code + + m_smbPkt.setLongErrorCode(errCode); + } + else + { + + // Disable the long error status flag + + if (m_smbPkt.isLongErrorCode() == true) + m_smbPkt.setFlags2(m_smbPkt.getFlags2() - SMBSrvPacket.FLG2_LONGERRORCODE); + + // Set the error status/class + + m_smbPkt.setErrorCode(errCode); + m_smbPkt.setErrorClass(errClass); + } + + // Return the error response to the client + + sendResponseSMB(m_smbPkt, m_smbPkt.getLength()); + + // Debug + + if (logger.isDebugEnabled() && hasDebug(DBG_ERROR)) + logger.debug("Error : Cmd = " + m_smbPkt.getPacketTypeString() + " - " + + SMBErrorText.ErrorString(errClass, errCode)); + } + + /** + * Send, or queue, an asynchronous response SMB + * + * @param pkt SMBSrvPacket + * @param len int + * @return true if the packet was sent, or false if it was queued + * @exception IOException If an I/O error occurs + */ + public final boolean sendAsynchResponseSMB(SMBSrvPacket pkt, int len) throws IOException + { + + // Check if there is an SMB currently being processed or pending data from the client + + boolean sts = false; + + if (m_rxlen == -1 && m_pktHandler.availableBytes() == 0) + { + + // Send the asynchronous response immediately + + sendResponseSMB(pkt, len); + m_pktHandler.flushPacket(); + + // Indicate that the SMB response has been sent + + sts = true; + } + else + { + + // Queue the packet to send out when current SMB requests have been processed + + queueAsynchResponseSMB(pkt); + } + + // Return the sent/queued status + + return sts; + } + + /** + * Queue an asynchronous response SMB for sending when current SMB requests have been processed. + * + * @param pkt SMBSrvPacket + */ + protected final synchronized void queueAsynchResponseSMB(SMBSrvPacket pkt) + { + + // Check if the asynchronous response queue has been allocated + + if (m_asynchQueue == null) + { + + // Allocate the asynchronous response queue + + m_asynchQueue = new Vector(); + } + + // Add the SMB response packet to the queue + + m_asynchQueue.addElement(pkt); + } + + /** + * Check if there are any asynchronous requests queued + * + * @return boolean + */ + protected final synchronized boolean hasAsynchResponse() + { + + // Check if the queue is valid + + if (m_asynchQueue != null && m_asynchQueue.size() > 0) + return true; + return false; + } + + /** + * Remove an asynchronous response packet from the head of the list + * + * @return SMBSrvPacket + */ + protected final synchronized SMBSrvPacket removeFirstAsynchResponse() + { + + // Check if there are asynchronous response packets queued + + if (m_asynchQueue == null || m_asynchQueue.size() == 0) + return null; + + // Return the SMB packet from the head of the queue + + return m_asynchQueue.remove(0); + } + + /** + * Find the notify request with the specified ids + * + * @param mid int + * @param tid int + * @param uid int + * @param pid int + * @return NotifyRequest + */ + public final NotifyRequest findNotifyRequest(int mid, int tid, int uid, int pid) + { + + // Check if the local notify list is valid + + if (m_notifyList == null) + return null; + + // Find the matching notify request + + return m_notifyList.findRequest(mid, tid, uid, pid); + } + + /** + * Find an existing notify request for the specified directory and filter + * + * @param dir NetworkFile + * @param filter int + * @param watchTree boolean + * @return NotifyRequest + */ + public final NotifyRequest findNotifyRequest(NetworkFile dir, int filter, boolean watchTree) + { + + // Check if the local notify list is valid + + if (m_notifyList == null) + return null; + + // Find the matching notify request + + return m_notifyList.findRequest(dir, filter, watchTree); + } + + /** + * Add a change notification request + * + * @param req NotifyRequest + * @param ctx DiskDeviceContext + */ + public final void addNotifyRequest(NotifyRequest req, DiskDeviceContext ctx) + { + + // Check if the local notify list has been allocated + + if (m_notifyList == null) + m_notifyList = new NotifyRequestList(); + + // Add the request to the local list and the shares global list + + m_notifyList.addRequest(req); + ctx.addNotifyRequest(req); + } + + /** + * Remove a change notification request + * + * @param req NotifyRequest + */ + public final void removeNotifyRequest(NotifyRequest req) + { + + // Check if the local notify list has been allocated + + if (m_notifyList == null) + return; + + // Remove the request from the local list and the shares global list + + m_notifyList.removeRequest(req); + if (req.getDiskContext() != null) + req.getDiskContext().removeNotifyRequest(req); + } + + /** + * Check if there is an active transaction + * + * @return boolean + */ + protected final boolean hasTransaction() + { + return m_transact != null ? true : false; + } + + /** + * Return the active transaction buffer + * + * @return TransactBuffer + */ + protected final SrvTransactBuffer getTransaction() + { + return m_transact; + } + + /** + * Set the active transaction buffer + * + * @param buf TransactBuffer + */ + protected final void setTransaction(SrvTransactBuffer buf) + { + m_transact = buf; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBSrvSessionState.java b/source/java/org/alfresco/filesys/smb/server/SMBSrvSessionState.java new file mode 100644 index 0000000000..1efd86351d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBSrvSessionState.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +/** + *

    + * Contains the various states that an SMB server session will go through during the session + * lifetime. + */ +public class SMBSrvSessionState +{ + + // NetBIOS session has been closed. + + public static final int NBHANGUP = 5; + + // NetBIOS session request state. + + public static final int NBSESSREQ = 0; + + // SMB session closed down. + + public static final int SMBCLOSED = 4; + + // Negotiate SMB dialect. + + public static final int SMBNEGOTIATE = 1; + + // SMB session is initialized, ready to receive/handle standard SMB requests. + + public static final int SMBSESSION = 3; + + // SMB session setup. + + public static final int SMBSESSSETUP = 2; + + // State name strings + + private static final String _stateName[] = { + "NBSESSREQ", + "SMBNEGOTIATE", + "SMBSESSSETUP", + "SMBSESSION", + "SMBCLOSED", + "NBHANGUP" }; + + /** + * Return the specified SMB state as a string. + */ + public static String getStateAsString(int state) + { + if (state < _stateName.length) + return _stateName[state]; + return "[UNKNOWN]"; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBSrvTransPacket.java b/source/java/org/alfresco/filesys/smb/server/SMBSrvTransPacket.java new file mode 100644 index 0000000000..927187b96b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBSrvTransPacket.java @@ -0,0 +1,838 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.TransactBuffer; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; + +/** + * SMB server transact packet class + */ +class SMBSrvTransPacket extends SMBTransPacket +{ + + // Define the number of standard parameters for a server response + + private static final int StandardParamsResponse = 10; + + // Offset to the setup response paramaters + + protected static final int SetupOffsetResponse = PARAMWORDS + (StandardParamsResponse * 2); + + /** + * Construct an SMB transaction packet + * + * @param buf Buffer that contains the SMB transaction packet. + */ + + public SMBSrvTransPacket(byte[] buf) + { + super(buf); + } + + /** + * Construct an SMB transaction packet + * + * @param siz Size of packet to allocate. + */ + + public SMBSrvTransPacket(int siz) + { + super(siz); + + // Set the multiplex id for this transaction + + setMultiplexId(getNextMultiplexId()); + } + + /** + * Initialize the transact reply parameters. + * + * @param pkt Reply SMB packet. + * @param prmCnt Count of returned parameter bytes. + * @param prmPos Starting offset to the parameter block. + * @param dataCnt Count of returned data bytes. + * @param dataPos Starting offset to the data block. + */ + public final static void initTransactReply(SMBSrvPacket pkt, int prmCnt, int prmPos, int dataCnt, int dataPos) + { + + // Set the total parameter words + + pkt.setParameterCount(10); + + // Set the total parameter/data bytes + + pkt.setParameter(0, prmCnt); + pkt.setParameter(1, dataCnt); + + // Clear the reserved parameter + + pkt.setParameter(2, 0); + + // Set the parameter byte count/offset for this packet + + pkt.setParameter(3, prmCnt); + pkt.setParameter(4, prmPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Set the parameter displacement + + pkt.setParameter(5, 0); + + // Set the data byte count/offset for this packet + + pkt.setParameter(6, dataCnt); + pkt.setParameter(7, dataPos - RFCNetBIOSProtocol.HEADER_LEN); + + // Set the data displacement + + pkt.setParameter(8, 0); + + // Set up word count + + pkt.setParameter(9, 0); + } + + /** + * Calculate the data item size from the data descriptor string. + * + * @param desc java.lang.String + * @return int + */ + protected final static int CalculateDataItemSize(String desc) + { + + // Scan the data descriptor string and calculate the data item size + + int len = 0; + int pos = 0; + + while (pos < desc.length()) + { + + // Get the current data item type + + char dtype = desc.charAt(pos++); + int dlen = 1; + + // Check if a data length has been specified + + if (pos < desc.length() && Character.isDigit(desc.charAt(pos))) + { + + // Convert the data length string + + int numlen = 1; + int numpos = pos + 1; + while (numpos < desc.length() && Character.isDigit(desc.charAt(numpos++))) + numlen++; + + // Set the data length + + dlen = Integer.parseInt(desc.substring(pos, pos + numlen)); + + // Update the descriptor string position + + pos = numpos - 1; + } + + // Convert the current data item + + switch (dtype) + { + + // Word (16 bit) data type + + case 'W': + len += 2; + break; + + // Integer (32 bit) data type + + case 'D': + len += 4; + break; + + // Byte data type, may be multiple bytes if 'B' + + case 'B': + len += dlen; + break; + + // Null terminated string data type, offset into buffer only + + case 'z': + len += 4; + break; + + // Skip 'n' bytes in the buffer + + case '.': + len += dlen; + break; + + // Integer (32 bit) data type converted to a date/time value + + case 'T': + len += 4; + break; + + } // end switch data type + + } // end while descriptor string + + // Return the data length of each item + + return len; + } + + /** + * Return the offset to the data block within the SMB packet. The data block is word aligned + * within the byte buffer area of the SMB packet. This method must be called after the parameter + * count has been set. + * + * @param prmLen Parameter block length, in bytes. + * @return int Offset to the data block area. + */ + public final int getDataBlockOffset(int prmLen) + { + + // Get the position of the parameter block + + int pos = getParameterBlockOffset() + prmLen; + if ((pos & 0x01) != 0) + pos++; + return pos; + } + + /** + * Return the data block offset. + * + * @return int Offset to data block within packet. + */ + public final int getRxDataBlock() + { + return getParameter(12) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the received transaction data block length. + * + * @return int + */ + public final int getRxDataBlockLength() + { + return getParameter(11); + } + + /** + * Get the required transact parameter word (16 bit). + * + * @param prmIdx int + * @return int + */ + public final int getRxParameter(int prmIdx) + { + + // Get the parameter block offset + + int pos = getRxParameterBlock(); + + // Get the required transact parameter word. + + pos += prmIdx * 2; // 16 bit words + return DataPacker.getIntelShort(getBuffer(), pos); + } + + /** + * Return the position of the parameter block within the received packet. + * + * @param prmblk Array to unpack the parameter block words into. + */ + + public final int getRxParameterBlock() + { + + // Get the offset to the parameter words, add the NetBIOS header length + // to the offset. + + return getParameter(10) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the received transaction parameter block length. + * + * @return int + */ + public final int getRxParameterBlockLength() + { + return getParameter(9); + } + + /** + * Return the received transaction setup parameter count. + * + * @return int + */ + public final int getRxParameterCount() + { + return getParameterCount() - STD_PARAMS; + } + + /** + * Get the required transact parameter int value (32-bit). + * + * @param prmIdx int + * @return int + */ + public final int getRxParameterInt(int prmIdx) + { + + // Get the parameter block offset + + int pos = getRxParameterBlock(); + + // Get the required transact parameter word. + + pos += prmIdx * 2; // 16 bit words + return DataPacker.getIntelInt(getBuffer(), pos); + } + + /** + * Get the required transact parameter string. + * + * @param pos Offset to the string within the parameter block. + * @param uni Unicode if true, else ASCII + * @return int + */ + public final String getRxParameterString(int pos, boolean uni) + { + + // Get the parameter block offset + + pos += getRxParameterBlock(); + + // Get the transact parameter string + + byte[] buf = getBuffer(); + int len = (buf[pos++] & 0x00FF); + return DataPacker.getString(buf, pos, len, uni); + } + + /** + * Get the required transact parameter string. + * + * @param pos Offset to the string within the parameter block. + * @param len Length of the string. + * @param uni Unicode if true, else ASCII + * @return int + */ + public final String getRxParameterString(int pos, int len, boolean uni) + { + + // Get the parameter block offset + + pos += getRxParameterBlock(); + + // Get the transact parameter string + + byte[] buf = getBuffer(); + return DataPacker.getString(buf, pos, len, uni); + } + + /** + * Return the received transaction name. + * + * @return java.lang.String + */ + public final String getRxTransactName() + { + + // Check if the transaction has a name + + if (getCommand() == PacketType.Transaction2) + return ""; + + // Unpack the transaction name string + + int pos = getByteOffset(); + return DataPacker.getString(getBuffer(), pos, getByteCount()); + } + + /** + * Return the setup parameter count + * + * @return int + */ + public final int getSetupCount() + { + return getParameter(13) & 0xFF; + } + + /** + * Return the buffer offset to the setup parameters + * + * @return int + */ + public final int getSetupOffset() + { + return WORDCNT + 29; // 14 setup words + word count byte + } + + /** + * Return the specified transaction setup parameter. + * + * @param idx Setup parameter index. + * @return int + */ + + public final int getSetupParameter(int idx) + { + + // Check if the setup parameter index is valid + + if (idx >= getRxParameterCount()) + throw new java.lang.ArrayIndexOutOfBoundsException(); + + // Get the setup parameter + + return getParameter(idx + STD_PARAMS); + } + + /** + * Return the maximum return paramater byte count + * + * @return int + */ + public final int getMaximumReturnParameterCount() + { + return getParameter(2); + } + + /** + * Return the maximum return data byte count + * + * @return int + */ + public final int getMaximumReturnDataCount() + { + return getParameter(3); + } + + /** + * Return the maximum return setup count + * + * @return int + */ + public final int getMaximumReturnSetupCount() + { + return getParameter(4); + } + + /** + * Return the specified transaction setup parameter 32bit value. + * + * @param idx Setup parameter index. + * @return int + */ + + public final int getSetupParameterInt(int idx) + { + + // Check if the setup parameter index is valid + + if (idx >= getRxParameterCount()) + throw new java.lang.ArrayIndexOutOfBoundsException(); + + // Get the setup parameter + + return getParameterLong(idx + STD_PARAMS); + } + + /** + * Set the total parameter block length, in bytes + * + * @param cnt int + */ + public final void setTotalParameterCount(int cnt) + { + setParameter(0, cnt); + } + + /** + * Set the total data block length, in bytes + * + * @param cnt int + */ + public final void setTotalDataCount(int cnt) + { + setParameter(1, cnt); + } + + /** + * Set the parameter block count for this packet + * + * @param len int + */ + public final void setParameterBlockCount(int len) + { + setParameter(3, len); + } + + /** + * Set the parameter block offset + * + * @param off int + */ + public final void setParameterBlockOffset(int off) + { + setParameter(4, off != 0 ? off - RFCNetBIOSProtocol.HEADER_LEN : 0); + } + + /** + * Set the parameter block displacement within the total parameter block + * + * @param disp int + */ + public final void setParameterBlockDisplacement(int disp) + { + setParameter(5, disp); + } + + /** + * Set the data block count for this packet + * + * @param len int + */ + public final void setDataBlockCount(int len) + { + setParameter(6, len); + } + + /** + * Set the data block offset, from the start of the packet + * + * @param off int + */ + public final void setDataBlockOffset(int off) + { + setParameter(7, off != 0 ? off - RFCNetBIOSProtocol.HEADER_LEN : 0); + } + + /** + * Set the data block displacement within the total data block + * + * @param disp int + */ + public final void setDataBlockDisplacement(int disp) + { + setParameter(8, disp); + } + + /** + * Send one or more transaction response SMBs to the client + * + * @param sess SMBSrvSession + * @param tbuf TransactBuffer + * @exception java.io.IOException If an I/O error occurs. + */ + protected final void doTransactionResponse(SMBSrvSession sess, TransactBuffer tbuf) throws IOException + { + + // Initialize the transaction response packet + + setCommand(tbuf.isType()); + + // Get the individual buffers from the transact buffer + + tbuf.setEndOfBuffer(); + + DataBuffer setupBuf = tbuf.getSetupBuffer(); + DataBuffer paramBuf = tbuf.getParameterBuffer(); + DataBuffer dataBuf = tbuf.getDataBuffer(); + + // Set the parameter count + + if (tbuf.hasSetupBuffer()) + setParameterCount(StandardParamsResponse + setupBuf.getLengthInWords()); + else + setParameterCount(StandardParamsResponse); + + // Clear the parameters + + for (int i = 0; i < getParameterCount(); i++) + setParameter(i, 0); + + // Get the total parameter/data block lengths + + int totParamLen = paramBuf != null ? paramBuf.getLength() : 0; + int totDataLen = dataBuf != null ? dataBuf.getLength() : 0; + + // Initialize the parameters + + setTotalParameterCount(totParamLen); + setTotalDataCount(totDataLen); + + // Get the available data space within the packet + + int availBuf = getAvailableLength(); + int clientLen = getAvailableLength(sess.getClientMaximumBufferSize()); + if (availBuf > clientLen) + availBuf = clientLen; + + // Check if the transaction parameter block and data block will fit within a single request + // packet + + int plen = totParamLen; + int dlen = totDataLen; + + if ((plen + dlen) > availBuf) + { + + // Calculate the parameter/data block sizes to send in the first request packet + + if (plen > 0) + { + + // Check if the parameter block can fit into the packet + + if (plen <= availBuf) + { + + // Pack all of the parameter block and fill the remaining buffer with the data + // block + + if (dlen > 0) + dlen = availBuf - plen; + } + else + { + + // Split the parameter/data space in the packet + + plen = availBuf / 2; + dlen = plen; + } + } + else if (dlen > availBuf) + { + + // Fill the packet with the first section of the data block + + dlen = availBuf; + } + } + + // Set the parameter/data block counts for this packet + + setParameterBlockCount(plen); + setDataBlockCount(dlen); + + // Pack the setup bytes + + if (setupBuf != null) + setupBuf.copyData(getBuffer(), SetupOffsetResponse); + + // Pack the parameter block + + int pos = DataPacker.wordAlign(getByteOffset()); + setPosition(pos); + + // Set the parameter block offset, from the start of the SMB packet + + setParameterBlockCount(plen); + setParameterBlockOffset(pos); + + int packLen = -1; + + if (paramBuf != null) + { + + // Pack the parameter block + + packLen = paramBuf.copyData(getBuffer(), pos, plen); + + // Update the buffer position for the data block + + pos = DataPacker.longwordAlign(pos + packLen); + setPosition(pos); + } + + // Set the data block offset + + setDataBlockCount(dlen); + setDataBlockOffset(pos); + + // Pack the data block + + if (dataBuf != null) + { + + // Pack the data block + + packLen = dataBuf.copyData(getBuffer(), pos, dlen); + + // Update the end of buffer position + + setPosition(pos + packLen); + } + + // Set the byte count for the SMB packet + + setByteCount(); + + // Send the start of the transaction request + + sess.sendResponseSMB(this); + + // Get the available parameter/data block buffer space for the secondary packet + + availBuf = getAvailableLength(); + if (availBuf > clientLen) + availBuf = clientLen; + + // Loop until all parameter/data block data has been sent to the server + + TransactBuffer rxBuf = null; + + while ((paramBuf != null && paramBuf.getAvailableLength() > 0) + || (dataBuf != null && dataBuf.getAvailableLength() > 0)) + { + + // Setup the NT transaction secondary packet to send the remaining parameter/data blocks + + setCommand(tbuf.isType()); + + // Get the remaining parameter/data block lengths + + plen = paramBuf != null ? paramBuf.getAvailableLength() : 0; + dlen = dataBuf != null ? dataBuf.getAvailableLength() : 0; + + if ((plen + dlen) > availBuf) + { + + // Calculate the parameter/data block sizes to send in the first request packet + + if (plen > 0) + { + + // Check if the remaining parameter block can fit into the packet + + if (plen <= availBuf) + { + + // Pack all of the parameter block and fill the remaining buffer with the + // data block + + if (dlen > 0) + dlen = availBuf - plen; + } + else + { + + // Split the parameter/data space in the packet + + plen = availBuf / 2; + dlen = plen; + } + } + else if (dlen > availBuf) + { + + // Fill the packet with the first section of the data block + + dlen = availBuf; + } + } + + // Pack the parameter block data, if any + + resetBytePointerAlign(); + + packLen = -1; + pos = getPosition(); + + if (plen > 0 && paramBuf != null) + { + + // Set the parameter block offset, from the start of the SMB packet + + setParameterBlockOffset(pos); + setParameterBlockCount(plen); + setParameterBlockDisplacement(paramBuf.getDisplacement()); + + // Pack the parameter block + + packLen = paramBuf.copyData(getBuffer(), pos, plen); + + // Update the buffer position for the data block + + pos = DataPacker.wordAlign(pos + packLen); + setPosition(pos); + } + else + { + + // No parameter data, clear the count/offset + + setParameterBlockCount(0); + setParameterBlockOffset(pos); + } + + // Pack the data block, if any + + if (dlen > 0 && dataBuf != null) + { + + // Set the data block offset + + setDataBlockOffset(pos); + setDataBlockCount(dlen); + setDataBlockDisplacement(dataBuf.getDisplacement()); + + // Pack the data block + + packLen = dataBuf.copyData(getBuffer(), pos, dlen); + + // Update the end of buffer position + + setPosition(pos + packLen); + } + else + { + + // No data, clear the count/offset + + setDataBlockCount(0); + setDataBlockOffset(pos); + } + + // Set the byte count for the SMB packet to set the overall length + + setByteCount(); + + // Send the transaction response packet + + sess.sendResponseSMB(this); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SMBTransPacket.java b/source/java/org/alfresco/filesys/smb/server/SMBTransPacket.java new file mode 100644 index 0000000000..17086690d3 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SMBTransPacket.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.util.DataPacker; + +/** + * SMB transact packet class + */ + +public class SMBTransPacket extends SMBSrvPacket +{ + + // Define the number of standard parameters + + protected static final int STD_PARAMS = 14; + + // Transaction status that indicates that this transaction has more data + // to be returned. + + public static final int IsContinued = 234; + + // Transact name, not used for transact 2 + + protected String m_transName; + + // Parameter count for this transaction + + protected int m_paramCnt; + + // Multiplex identifier, to identify each transaction request + + private static int m_nextMID = 1; + + /** + * Construct an SMB transaction packet + * + * @param buf Buffer that contains the SMB transaction packet. + */ + public SMBTransPacket(byte[] buf) + { + super(buf); + } + + /** + * Construct an SMB transaction packet + * + * @param siz Size of packet to allocate. + */ + public SMBTransPacket(int siz) + { + super(siz); + + // Set the multiplex id for this transaction + + setMultiplexId(getNextMultiplexId()); + } + + /** + * Get the next multiplex id to uniquely identify this transaction + * + * @return Unique multiplex id for this transaction + */ + public final static int getNextMultiplexId() + { + return m_nextMID++; + } + + /** + * Return the total parameter byte count + * + * @return int + */ + public final int getTotalParameterCount() + { + return getParameter(0); + } + + /** + * Return the total data byte count + * + * @return int + */ + public final int getTotalDataCount() + { + return getParameter(1); + } + + /** + * Return the parameter count size in bytes for this section + * + * @return int + */ + public final int getParameterBlockCount() + { + return getParameter(9); + } + + /** + * Return the parameter block offset + * + * @return Paramter block offset within the SMB packet + */ + public final int getParameterBlockOffset() + { + return getParameter(10) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the data block size in bytes for this section + * + * @return int + */ + public final int getDataBlockCount() + { + return getParameter(11); + } + + /** + * Return the data block offset + * + * @return Data block offset within the SMB packet. + */ + public final int getDataBlockOffset() + { + return getParameter(12) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the secondary parameter block size in bytes + * + * @return int + */ + public final int getSecondaryParameterBlockCount() + { + return getParameter(2); + } + + /** + * Return the secondary parameter block offset + * + * @return int + */ + public final int getSecondaryParameterBlockOffset() + { + return getParameter(3) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the secondary parameter block displacement + * + * @return int + */ + public final int getParameterBlockDisplacement() + { + return getParameter(4); + } + + /** + * Return the secondary data block size in bytes + * + * @return int + */ + public final int getSecondaryDataBlockCount() + { + return getParameter(5); + } + + /** + * Return the secondary data block offset + * + * @return int + */ + public final int getSecondaryDataBlockOffset() + { + return getParameter(6) + RFCNetBIOSProtocol.HEADER_LEN; + } + + /** + * Return the secondary data block displacement + * + * @return int + */ + public final int getDataBlockDisplacement() + { + return getParameter(7); + } + + /** + * Return the transaction sub-command + * + * @return int + */ + public final int getSubFunction() + { + return getParameter(14); + } + + /** + * Unpack the parameter block into the supplied array. + * + * @param prmblk Array to unpack the parameter block words into. + */ + public final void getParameterBlock(short[] prmblk) throws java.lang.ArrayIndexOutOfBoundsException + { + + // Determine how many parameters are to be unpacked, check if the user + // buffer is long enough + + int prmcnt = getParameter(3) / 2; // convert to number of words + if (prmblk.length < prmcnt) + throw new java.lang.ArrayIndexOutOfBoundsException(); + + // Get the offset to the parameter words, add the NetBIOS header length + // to the offset. + + int pos = getParameter(4) + RFCNetBIOSProtocol.HEADER_LEN; + + // Unpack the parameter words + + byte[] buf = getBuffer(); + + for (int idx = 0; idx < prmcnt; idx++) + { + + // Unpack the current parameter word + + prmblk[idx] = (short) DataPacker.getIntelShort(buf, pos); + pos += 2; + } + } + + /** + * Initialize the transact SMB packet + * + * @param pcnt Total parameter count for this transaction + * @param paramblk Parameter block data bytes + * @param plen Parameter block data length + * @param datablk Data block data bytes + * @param dlen Data block data length + */ + public final void InitializeTransact(int pcnt, byte[] paramblk, int plen, byte[] datablk, int dlen) + { + + // Set the SMB command code + + if (m_transName == null) + setCommand(PacketType.Transaction2); + else + setCommand(PacketType.Transaction); + + // Set the parameter count + + setParameterCount(pcnt); + + // Save the parameter count, add an extra parameter for the data byte count + + m_paramCnt = pcnt; + + // Initialize the parameters + + setParameter(0, plen); // total parameter bytes being sent + setParameter(1, dlen); // total data bytes being sent + + for (int i = 2; i < 9; setParameter(i++, 0)) + ; + + setParameter(9, plen); // parameter bytes sent in this packet + setParameter(11, dlen); // data bytes sent in this packet + + setParameter(13, pcnt - STD_PARAMS); // number of setup words + + // Get the data byte offset + + int pos = getByteOffset(); + int startPos = pos; + + // Check if this is a named transaction, if so then store the name + + int idx; + byte[] buf = getBuffer(); + + if (m_transName != null) + { + + // Store the transaction name + + byte[] nam = m_transName.getBytes(); + + for (idx = 0; idx < nam.length; idx++) + buf[pos++] = nam[idx]; + } + + // Word align the buffer offset + + if ((pos % 2) > 0) + pos++; + + // Store the parameter block + + if (paramblk != null) + { + + // Set the parameter block offset + + setParameter(10, pos - RFCNetBIOSProtocol.HEADER_LEN); + + // Store the parameter block + + for (idx = 0; idx < plen; idx++) + buf[pos++] = paramblk[idx]; + } + else + { + + // Clear the parameter block offset + + setParameter(10, 0); + } + + // Word align the data block + + if ((pos % 2) > 0) + pos++; + + // Store the data block + + if (datablk != null) + { + + // Set the data block offset + + setParameter(12, pos - RFCNetBIOSProtocol.HEADER_LEN); + + // Store the data block + + for (idx = 0; idx < dlen; idx++) + buf[pos++] = datablk[idx]; + } + else + { + + // Zero the data block offset + + setParameter(12, 0); + } + + // Set the byte count for the SMB packet + + setByteCount(pos - startPos); + } + + /** + * Set the specifiec setup parameter within the SMB packet. + * + * @param idx Setup parameter index. + * @param val Setup parameter value. + */ + + public final void setSetupParameter(int idx, int val) + { + setParameter(STD_PARAMS + idx, val); + } + + /** + * Set the transaction name for normal transactions + * + * @param tname Transaction name string + */ + + public final void setTransactionName(String tname) + { + m_transName = tname; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/SessionSocketHandler.java b/source/java/org/alfresco/filesys/smb/server/SessionSocketHandler.java new file mode 100644 index 0000000000..4d869c2154 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SessionSocketHandler.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.SocketException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Session Socket Handler Abstract Class + * + * @author GKSpencer + */ +public abstract class SessionSocketHandler implements Runnable +{ + // Debug logging + + protected static Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Define the listen backlog for the server socket + + protected static final int LISTEN_BACKLOG = 10; + + // Server that the socket handler is associated with + + private SMBServer m_server; + + // Address/post to use + + private int m_port; + private InetAddress m_bindAddr; + + // Server socket + + private ServerSocket m_srvSock; + + // Debug output enable + + private boolean m_debug; + + // Socket handler thread shutdown flag + + private boolean m_shutdown; + + // Session socket handler name + + private String m_name; + + // Session id + + private static int m_sessId; + + /** + * Class constructor + * + * @param name String + * @param srv SMBServer + * @param port int + * @param bindAddr InetAddress + * @param debug boolean + */ + public SessionSocketHandler(String name, SMBServer srv, int port, InetAddress bindAddr, boolean debug) + { + m_name = name; + m_server = srv; + m_port = port; + m_bindAddr = bindAddr; + m_debug = debug; + } + + /** + * Class constructor + * + * @param name String + * @param srv SMBServer + * @param debug boolean + */ + public SessionSocketHandler(String name, SMBServer srv, boolean debug) + { + m_name = name; + m_server = srv; + m_debug = debug; + } + + /** + * Return the handler name + * + * @return String + */ + public final String getName() + { + return m_name; + } + + /** + * Return the server + * + * @return SMBServer + */ + protected final SMBServer getServer() + { + return m_server; + } + + /** + * Return the port + * + * @return int + */ + protected final int getPort() + { + return m_port; + } + + /** + * Determine if the socket handler should bind to a particular address + * + * @return boolean + */ + protected final boolean hasBindAddress() + { + return m_bindAddr != null ? true : false; + } + + /** + * Return the bind address return InetAddress + */ + protected final InetAddress getBindAddress() + { + return m_bindAddr; + } + + /** + * Return the next session id + * + * @return int + */ + protected final synchronized int getNextSessionId() + { + return m_sessId++; + } + + /** + * Determine if debug output is enabled + * + * @return boolean + */ + protected final boolean hasDebug() + { + return m_debug; + } + + /** + * Return the server socket + * + * @return ServerSocket + */ + protected final ServerSocket getSocket() + { + return m_srvSock; + } + + /** + * Set the server socket + * + * @param sock ServerSocket + */ + protected final void setSocket(ServerSocket sock) + { + m_srvSock = sock; + } + + /** + * Determine if the shutdown flag is set + * + * @return boolean + */ + protected final boolean hasShutdown() + { + return m_shutdown; + } + + /** + * Clear the shutdown request flag + */ + protected final void clearShutdown() + { + m_shutdown = false; + } + + /** + * Request the socket handler to shutdown + */ + public void shutdownRequest() + { + + // Indicate that the server is closing + + m_shutdown = true; + + try + { + + // Close the server socket so that any pending receive is cancelled + + if (m_srvSock != null) + m_srvSock.close(); + } + catch (SocketException ex) + { + } + catch (Exception ex) + { + } + } + + /** + * Initialize the session socket handler + * + * @exception Exception + */ + public void initialize() throws Exception + { + + // Check if the server should bind to a particular local address, or all local addresses + + ServerSocket srvSock = null; + + if (hasBindAddress()) + srvSock = new ServerSocket(getPort(), LISTEN_BACKLOG, getBindAddress()); + else + srvSock = new ServerSocket(getPort(), LISTEN_BACKLOG); + setSocket(srvSock); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Binding " + getName() + " session handler to local address : " + + (hasBindAddress() ? getBindAddress().getHostAddress() : "ALL")); + } + + /** + * @see Runnable#run() + */ + public abstract void run(); + + /** + * Return the session socket handler as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getName()); + str.append(","); + str.append(getServer().getServerName()); + str.append(","); + str.append(getBindAddress() != null ? getBindAddress().getHostAddress() : ""); + str.append(":"); + str.append(getPort()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/SrvSessionFactory.java b/source/java/org/alfresco/filesys/smb/server/SrvSessionFactory.java new file mode 100644 index 0000000000..619681de9f --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SrvSessionFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +/** + * Server Session Factory Interface + */ +public interface SrvSessionFactory +{ + + /** + * Create a new server session object + * + * @param handler PacketHandler + * @param server SMBServer + * @return SMBSrvSession + */ + public SMBSrvSession createSession(PacketHandler handler, SMBServer server); +} diff --git a/source/java/org/alfresco/filesys/smb/server/SrvTransactBuffer.java b/source/java/org/alfresco/filesys/smb/server/SrvTransactBuffer.java new file mode 100644 index 0000000000..3fcf19384d --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/SrvTransactBuffer.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.TransactBuffer; +import org.alfresco.filesys.util.DataBuffer; +import org.alfresco.filesys.util.DataPacker; + +/** + * Transact Buffer Class + *

    + * Contains the parameters and data for a transaction, transaction2 or NT transaction request. + */ +class SrvTransactBuffer extends TransactBuffer +{ + + /** + * Class constructor + * + * @param slen int + * @param plen int + * @param dlen int + */ + public SrvTransactBuffer(int slen, int plen, int dlen) + { + super(slen, plen, dlen); + } + + /** + * Class constructor + *

    + * Construct a TransactBuffer using the maximum size settings from the specified transaction + * buffer + * + * @param tbuf SrvTransactBuffer + */ + public SrvTransactBuffer(SrvTransactBuffer tbuf) + { + super(tbuf.getReturnSetupLimit(), tbuf.getReturnParameterLimit(), tbuf.getReturnDataLimit()); + + // Save the return limits for this transaction buffer + + setReturnLimits(tbuf.getReturnSetupLimit(), tbuf.getReturnParameterLimit(), tbuf.getReturnDataLimit()); + + // Set the transaction reply type + + setType(tbuf.isType()); + + // Copy the tree id + + setTreeId(tbuf.getTreeId()); + } + + /** + * Class constructor + * + * @param ntpkt NTTransPacket + */ + public SrvTransactBuffer(NTTransPacket ntpkt) + { + + // Call the base constructor so that it does not allocate any buffers + + super(0, 0, 0); + + // Set the tree id + + setTreeId(ntpkt.getTreeId()); + + // Set the setup block and size + + int slen = ntpkt.getSetupCount() * 2; + if (slen > 0) + m_setupBuf = new DataBuffer(ntpkt.getBuffer(), ntpkt.getSetupOffset(), slen); + + // Set the parameter block and size + + int plen = ntpkt.getTotalParameterCount(); + if (plen > 0) + m_paramBuf = new DataBuffer(ntpkt.getBuffer(), ntpkt.getParameterBlockOffset(), plen); + + // Set the data block and size + + int dlen = ntpkt.getDataBlockCount(); + if (dlen > 0) + m_dataBuf = new DataBuffer(ntpkt.getBuffer(), ntpkt.getDataBlockOffset(), dlen); + + // Set the transaction type and sub-function + + setType(ntpkt.getCommand()); + setFunction(ntpkt.getNTFunction()); + + // Set the maximum parameter and data block lengths to be returned + + setReturnParameterLimit(ntpkt.getMaximumParameterReturn()); + setReturnDataLimit(ntpkt.getMaximumDataReturn()); + + // Set the Unicode flag + + setUnicode(ntpkt.isUnicode()); + + // Indicate that this is a not a multi-packet transaction + + m_multi = false; + } + + /** + * Class constructor + * + * @param tpkt SMBSrvTransPacket + */ + public SrvTransactBuffer(SMBSrvTransPacket tpkt) + { + + // Call the base constructor so that it does not allocate any buffers + + super(0, 0, 0); + + // Set the tree id + + setTreeId(tpkt.getTreeId()); + + // Set the setup block and size + + int slen = tpkt.getSetupCount() * 2; + if (slen > 0) + m_setupBuf = new DataBuffer(tpkt.getBuffer(), tpkt.getSetupOffset(), slen); + + // Set the parameter block and size + + int plen = tpkt.getTotalParameterCount(); + if (plen > 0) + m_paramBuf = new DataBuffer(tpkt.getBuffer(), tpkt.getRxParameterBlock(), plen); + + // Set the data block and size + + int dlen = tpkt.getRxDataBlockLength(); + if (dlen > 0) + m_dataBuf = new DataBuffer(tpkt.getBuffer(), tpkt.getRxDataBlock(), dlen); + + // Set the transaction type and sub-function + + setType(tpkt.getCommand()); + + if (tpkt.getSetupCount() > 0) + setFunction(tpkt.getSetupParameter(0)); + + // Set the Unicode flag + + setUnicode(tpkt.isUnicode()); + + // Get the transaction name, if used + + if (isType() == PacketType.Transaction) + { + + // Unpack the transaction name string + + int pos = tpkt.getByteOffset(); + byte[] buf = tpkt.getBuffer(); + + if (isUnicode()) + pos = DataPacker.wordAlign(pos); + + setName(DataPacker.getString(buf, pos, 64, isUnicode())); + } + else + setName(""); + + // Indicate that this is a not a multi-packet transaction + + m_multi = false; + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/TcpipSMBPacketHandler.java b/source/java/org/alfresco/filesys/smb/server/TcpipSMBPacketHandler.java new file mode 100644 index 0000000000..55dd7198ad --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/TcpipSMBPacketHandler.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.io.IOException; +import java.net.Socket; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.util.DataPacker; + +/** + * Tcpip SMB Packet Handler Class + */ +public class TcpipSMBPacketHandler extends PacketHandler +{ + + /** + * Class constructor + * + * @param sock Socket + * @exception IOException If a network error occurs + */ + public TcpipSMBPacketHandler(Socket sock) throws IOException + { + super(sock, SMBSrvPacket.PROTOCOL_TCPIP, "TCP-SMB", "T"); + } + + /** + * Read a packet from the input stream + * + * @param pkt SMBSrvPacket + * @return int + * @exception IOexception If a network error occurs + */ + public int readPacket(SMBSrvPacket pkt) throws IOException + { + + // Read the packet header + + byte[] buf = pkt.getBuffer(); + int len = 0; + + while (len < RFCNetBIOSProtocol.HEADER_LEN && len != -1) + len = readPacket(buf, len, RFCNetBIOSProtocol.HEADER_LEN - len); + + // Check if the connection has been closed, read length equals -1 + + if (len == -1) + return len; + + // Check if we received a valid header + + if (len < RFCNetBIOSProtocol.HEADER_LEN) + throw new IOException("Invalid header, len=" + len); + + // Get the packet type from the header + + int typ = (int) (buf[0] & 0xFF); + int dlen = (int) DataPacker.getShort(buf, 2); + + // Check for a large packet, add to the data length + + if (buf[1] != 0) + { + int llen = (int) buf[1]; + dlen += (llen << 16); + } + + // Check if the packet buffer is large enough to hold the data + header + + if (buf.length < (dlen + RFCNetBIOSProtocol.HEADER_LEN)) + { + + // Allocate a new buffer to hold the data and copy the existing header + + byte[] newBuf = new byte[dlen + RFCNetBIOSProtocol.HEADER_LEN]; + for (int i = 0; i < 4; i++) + newBuf[i] = buf[i]; + + // Attach the new buffer to the SMB packet + + pkt.setBuffer(newBuf); + buf = newBuf; + } + + // Read the data part of the packet into the users buffer, this may take + // several reads + + int offset = RFCNetBIOSProtocol.HEADER_LEN; + int totlen = offset; + + while (dlen > 0) + { + + // Read the data + + len = readPacket(buf, offset, dlen); + + // Check if the connection has been closed + + if (len == -1) + return -1; + + // Update the received length and remaining data length + + totlen += len; + dlen -= len; + + // Update the user buffer offset as more reads will be required + // to complete the data read + + offset += len; + + } // end while reading data + + // Return the received packet length + + return totlen; + } + + /** + * Send a packet to the output stream + * + * @param pkt SMBSrvPacket + * @param len int + * @exception IOexception If a network error occurs + */ + public void writePacket(SMBSrvPacket pkt, int len) throws IOException + { + + // Fill in the TCP SMB message header, this is already allocated as + // part of the users buffer. + + byte[] buf = pkt.getBuffer(); + DataPacker.putInt(len, buf, 0); + + // Output the data packet + + int bufSiz = len + RFCNetBIOSProtocol.HEADER_LEN; + writePacket(buf, 0, bufSiz); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/TcpipSMBSessionSocketHandler.java b/source/java/org/alfresco/filesys/smb/server/TcpipSMBSessionSocketHandler.java new file mode 100644 index 0000000000..79aaf90123 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/TcpipSMBSessionSocketHandler.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server; + +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; + +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.smb.TcpipSMB; + +/** + * Native SMB Session Socket Handler Class + */ +public class TcpipSMBSessionSocketHandler extends SessionSocketHandler +{ + + /** + * Class constructor + * + * @param srv SMBServer + * @param port int + * @param bindAddr InetAddress + * @param debug boolean + */ + public TcpipSMBSessionSocketHandler(SMBServer srv, int port, InetAddress bindAddr, boolean debug) + { + super("TCP-SMB", srv, port, bindAddr, debug); + } + + /** + * Run the native SMB session socket handler + */ + public void run() + { + + try + { + + // Clear the shutdown flag + + clearShutdown(); + + // Wait for incoming connection requests + + while (hasShutdown() == false) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Waiting for TCP-SMB session request ..."); + + // Wait for a connection + + Socket sessSock = getSocket().accept(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] TCP-SMB session request received from " + + sessSock.getInetAddress().getHostAddress()); + + try + { + + // Create a packet handler for the session + + PacketHandler pktHandler = new TcpipSMBPacketHandler(sessSock); + + // Create a server session for the new request, and set the session id. + + SMBSrvSession srvSess = new SMBSrvSession(pktHandler, getServer()); + srvSess.setSessionId(getNextSessionId()); + srvSess.setUniqueId(pktHandler.getShortName() + srvSess.getSessionId()); + srvSess.setDebugPrefix("[" + pktHandler.getShortName() + srvSess.getSessionId() + "] "); + + // Add the session to the active session list + + getServer().addSession(srvSess); + + // Start the new session in a seperate thread + + Thread srvThread = new Thread(srvSess); + srvThread.setDaemon(true); + srvThread.setName("Sess_T" + srvSess.getSessionId() + "_" + + sessSock.getInetAddress().getHostAddress()); + srvThread.start(); + } + catch (Exception ex) + { + + // Debug + + logger.error("[SMB] TCP-SMB Failed to create session, ", ex); + } + } + } + catch (SocketException ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + logger.error("[SMB] TCP-SMB Socket error : ", ex); + } + catch (Exception ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + logger.error("[SMB] TCP-SMB Server error : ", ex); + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] TCP-SMB session handler closed"); + } + + /** + * Create the TCP/IP native SMB/CIFS session socket handlers for the main SMB/CIFS server + * + * @param server SMBServer + * @param sockDbg boolean + * @exception Exception + */ + public final static void createSessionHandlers(SMBServer server, boolean sockDbg) throws Exception + { + + // Access the server configuration + + ServerConfiguration config = server.getConfiguration(); + + // Create the NetBIOS SMB handler + + SessionSocketHandler sessHandler = new TcpipSMBSessionSocketHandler(server, TcpipSMB.PORT, config + .getSMBBindAddress(), sockDbg); + + sessHandler.initialize(); + server.addSessionHandler(sessHandler); + + // Run the TCP/IP SMB session handler in a seperate thread + + Thread tcpThread = new Thread(sessHandler); + tcpThread.setName("TcpipSMB_Handler"); + tcpThread.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] Native SMB TCP session handler created"); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEvent.java b/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEvent.java new file mode 100644 index 0000000000..e944a0cdd0 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEvent.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.notify; + +import org.alfresco.filesys.server.filesys.NotifyChange; + +/** + * Notify Change Event Class + *

    + * Contains the details of a change notification event + */ +public class NotifyChangeEvent +{ + + // Notification event action and filter type + + private int m_action; + private int m_filter; + + // Notification file/directory name + + private String m_fileName; + + // Path is a directory + + private boolean m_dir; + + // Original file name for file/directory rename + + private String m_oldName; + + /** + * Class constructor + * + * @param filter int + * @param action int + * @param fname String + * @param dir boolean + */ + public NotifyChangeEvent(int filter, int action, String fname, boolean dir) + { + m_filter = filter; + m_action = action; + m_fileName = fname; + m_dir = dir; + + // Normalize the path + + if (m_fileName.indexOf('/') != -1) + m_fileName.replace('/', '\\'); + } + + /** + * Class constructor + * + * @param filter int + * @param action int + * @param fname String + * @param oldname String + * @param dir boolean + */ + public NotifyChangeEvent(int filter, int action, String fname, String oldname, boolean dir) + { + m_filter = filter; + m_action = action; + m_fileName = fname; + m_oldName = oldname; + m_dir = dir; + + // Normalize the path + + if (m_fileName.indexOf('/') != -1) + m_fileName.replace('/', '\\'); + + if (m_oldName.indexOf('/') != -1) + m_oldName.replace('/', '\\'); + } + + /** + * Return the event filter type + * + * @return int + */ + public final int getFilter() + { + return m_filter; + } + + /** + * Return the action + * + * @return int + */ + public final int getAction() + { + return m_action; + } + + /** + * Return the file/directory name + * + * @return String + */ + public final String getFileName() + { + return m_fileName; + } + + /** + * Return the file/directory name only by stripping any leading path + * + * @return String + */ + public final String getShortFileName() + { + + // Find the last '\' in the path string + + int pos = m_fileName.lastIndexOf("\\"); + if (pos != -1) + return m_fileName.substring(pos + 1); + return m_fileName; + } + + /** + * Return the old file/directory name, for rename events + * + * @return String + */ + public final String getOldFileName() + { + return m_oldName; + } + + /** + * Return the old file/directory name only by stripping any leading path + * + * @return String + */ + public final String getShortOldFileName() + { + + // Check if the old path string is valid + + if (m_oldName == null) + return null; + + // Find the last '\' in the path string + + int pos = m_oldName.lastIndexOf("\\"); + if (pos != -1) + return m_oldName.substring(pos + 1); + return m_oldName; + } + + /** + * Check if the old file/directory name is valid + * + * @return boolean + */ + public final boolean hasOldFileName() + { + return m_oldName != null ? true : false; + } + + /** + * Check if the path refers to a directory + * + * @return boolean + */ + public final boolean isDirectory() + { + return m_dir; + } + + /** + * Return the notify change event as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(NotifyChange.getFilterAsString(getFilter())); + str.append("-"); + str.append(NotifyChange.getActionAsString(getAction())); + str.append(":"); + str.append(getFileName()); + + if (isDirectory()) + str.append(",DIR"); + + if (hasOldFileName()) + { + str.append(",Old="); + str.append(getOldFileName()); + } + + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEventList.java b/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEventList.java new file mode 100644 index 0000000000..3e76afab66 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeEventList.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.notify; + +import java.util.Vector; + +/** + * Notify Change Event List Class + */ +public class NotifyChangeEventList +{ + + // List of notify events + + private Vector m_list; + + /** + * Default constructor + */ + public NotifyChangeEventList() + { + m_list = new Vector(); + } + + /** + * Return the count of notify events + * + * @return int + */ + public final int numberOfEvents() + { + return m_list.size(); + } + + /** + * Return the specified change event + * + * @param idx int + * @return NotifyChangeEvent + */ + public final NotifyChangeEvent getEventAt(int idx) + { + + // Range check the index + + if (idx < 0 || idx >= m_list.size()) + return null; + + // Return the required notify event + + return m_list.get(idx); + } + + /** + * Add a change event to the list + * + * @param evt NotifyChangeEvent + */ + public final void addEvent(NotifyChangeEvent evt) + { + m_list.add(evt); + } + + /** + * Remove the specified change event + * + * @param idx int + * @return NotifyChangeEvent + */ + public final NotifyChangeEvent removeEventAt(int idx) + { + + // Range check the index + + if (idx < 0 || idx >= m_list.size()) + return null; + + // Return the required notify event + + return m_list.remove(idx); + } + + /** + * Remove all events from the list + */ + public final void removeAllEvents() + { + m_list.removeAllElements(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeHandler.java b/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeHandler.java new file mode 100644 index 0000000000..c9fff40f87 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/notify/NotifyChangeHandler.java @@ -0,0 +1,1119 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.notify; + +import java.util.Vector; + +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.filesys.server.filesys.NotifyChange; +import org.alfresco.filesys.smb.PacketType; +import org.alfresco.filesys.smb.server.NTTransPacket; +import org.alfresco.filesys.smb.server.SMBSrvPacket; +import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.alfresco.filesys.util.DataPacker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Notify Change Handler Class + */ +public class NotifyChangeHandler implements Runnable +{ + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Change notification request list and global filter mask + + private NotifyRequestList m_notifyList; + private int m_globalNotifyMask; + + // Associated disk device context + + private DiskDeviceContext m_diskCtx; + + // Change notification processing thread + + private Thread m_procThread; + + // Change events queue + + private NotifyChangeEventList m_eventList; + + // Debug output enable + + private boolean m_debug = false; + + // Shutdown request flag + + private boolean m_shutdown; + + /** + * Class constructor + * + * @param diskCtx DiskDeviceContext + */ + public NotifyChangeHandler(DiskDeviceContext diskCtx) + { + + // Save the associated disk context details + + m_diskCtx = diskCtx; + + // Allocate the events queue + + m_eventList = new NotifyChangeEventList(); + + // Create the processing thread + + m_procThread = new Thread(this); + + m_procThread.setDaemon(true); + m_procThread.setName("Notify_" + m_diskCtx.getDeviceName()); + + m_procThread.start(); + } + + /** + * Add a request to the change notification list + * + * @param req NotifyRequest + */ + public final void addNotifyRequest(NotifyRequest req) + { + + // Check if the request list has been allocated + + if (m_notifyList == null) + m_notifyList = new NotifyRequestList(); + + // Add the request to the list + + req.setDiskContext(m_diskCtx); + m_notifyList.addRequest(req); + + // Regenerate the global notify change filter mask + + m_globalNotifyMask = m_notifyList.getGlobalFilter(); + } + + /** + * Remove a request from the notify change request list + * + * @param req NotifyRequest + */ + public final void removeNotifyRequest(NotifyRequest req) + { + removeNotifyRequest(req, true); + } + + /** + * Remove a request from the notify change request list + * + * @param req NotifyRequest + * @param updateMask boolean + */ + public final void removeNotifyRequest(NotifyRequest req, boolean updateMask) + { + + // Check if the request list has been allocated + + if (m_notifyList == null) + return; + + // Remove the request from the list + + m_notifyList.removeRequest(req); + + // Regenerate the global notify change filter mask + + if (updateMask == true) + m_globalNotifyMask = m_notifyList.getGlobalFilter(); + } + + /** + * Remove all notification requests owned by the specified session + * + * @param sess SMBSrvSession + */ + public final void removeNotifyRequests(SMBSrvSession sess) + { + + // Remove all requests owned by the session + + m_notifyList.removeAllRequestsForSession(sess); + + // Recalculate the global notify change filter mask + + m_globalNotifyMask = m_notifyList.getGlobalFilter(); + } + + /** + * Determine if the filter has file name change notification, triggered if a file is created, + * renamed or deleted + * + * @return boolean + */ + public final boolean hasFileNameChange() + { + return hasFilterFlag(NotifyChange.FileName); + } + + /** + * Determine if the filter has directory name change notification, triggered if a directory is + * created or deleted. + * + * @return boolean + */ + public final boolean hasDirectoryNameChange() + { + return hasFilterFlag(NotifyChange.DirectoryName); + } + + /** + * Determine if the filter has attribute change notification + * + * @return boolean + */ + public final boolean hasAttributeChange() + { + return hasFilterFlag(NotifyChange.Attributes); + } + + /** + * Determine if the filter has file size change notification + * + * @return boolean + */ + public final boolean hasFileSizeChange() + { + return hasFilterFlag(NotifyChange.Size); + } + + /** + * Determine if the filter has last write time change notification + * + * @return boolean + */ + public final boolean hasFileWriteTimeChange() + { + return hasFilterFlag(NotifyChange.LastWrite); + } + + /** + * Determine if the filter has last access time change notification + * + * @return boolean + */ + public final boolean hasFileAccessTimeChange() + { + return hasFilterFlag(NotifyChange.LastAccess); + } + + /** + * Determine if the filter has creation time change notification + * + * @return boolean + */ + public final boolean hasFileCreateTimeChange() + { + return hasFilterFlag(NotifyChange.Creation); + } + + /** + * Determine if the filter has the security descriptor change notification + * + * @return boolean + */ + public final boolean hasSecurityDescriptorChange() + { + return hasFilterFlag(NotifyChange.Security); + } + + /** + * Check if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Return the global notify filter mask + * + * @return int + */ + public final int getGlobalNotifyMask() + { + return m_globalNotifyMask; + } + + /** + * Return the notify request queue size + * + * @return int + */ + public final int getRequestQueueSize() + { + return m_notifyList != null ? m_notifyList.numberOfRequests() : 0; + } + + /** + * Check if the change filter has the specified flag enabled + * + * @param flag + * @return boolean + */ + private final boolean hasFilterFlag(int flag) + { + return (m_globalNotifyMask & flag) != 0 ? true : false; + } + + /** + * File changed notification + * + * @param action int + * @param path String + */ + public final void notifyFileChanged(int action, String path) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasFileNameChange() == false) + return; + + // Queue the change notification + + queueNotification(new NotifyChangeEvent(NotifyChange.FileName, action, path, false)); + } + + /** + * File/directory renamed notification + * + * @param oldName String + * @param newName String + */ + public final void notifyRename(String oldName, String newName) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || (hasFileNameChange() == false && hasDirectoryNameChange() == false)) + return; + + // Queue the change notification event + + queueNotification(new NotifyChangeEvent(NotifyChange.FileName, NotifyChange.ActionRenamedNewName, oldName, + newName, false)); + } + + /** + * Directory changed notification + * + * @param action int + * @param path String + */ + public final void notifyDirectoryChanged(int action, String path) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasDirectoryNameChange() == false) + return; + + // Queue the change notification event + + queueNotification(new NotifyChangeEvent(NotifyChange.DirectoryName, action, path, true)); + } + + /** + * Attributes changed notification + * + * @param path String + * @param isdir boolean + */ + public final void notifyAttributesChanged(String path, boolean isdir) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasAttributeChange() == false) + return; + + // Queue the change notification event + + queueNotification(new NotifyChangeEvent(NotifyChange.Attributes, NotifyChange.ActionModified, path, isdir)); + } + + /** + * File size changed notification + * + * @param path String + */ + public final void notifyFileSizeChanged(String path) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasFileSizeChange() == false) + return; + + // Send the change notification + + queueNotification(new NotifyChangeEvent(NotifyChange.Size, NotifyChange.ActionModified, path, false)); + } + + /** + * Last write time changed notification + * + * @param path String + * @param isdir boolean + */ + public final void notifyLastWriteTimeChanged(String path, boolean isdir) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasFileWriteTimeChange() == false) + return; + + // Send the change notification + + queueNotification(new NotifyChangeEvent(NotifyChange.LastWrite, NotifyChange.ActionModified, path, isdir)); + } + + /** + * Last access time changed notification + * + * @param path String + * @param isdir boolean + */ + public final void notifyLastAccessTimeChanged(String path, boolean isdir) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasFileAccessTimeChange() == false) + return; + + // Send the change notification + + queueNotification(new NotifyChangeEvent(NotifyChange.LastAccess, NotifyChange.ActionModified, path, isdir)); + } + + /** + * Creation time changed notification + * + * @param path String + * @param isdir boolean + */ + public final void notifyCreationTimeChanged(String path, boolean isdir) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasFileCreateTimeChange() == false) + return; + + // Send the change notification + + queueNotification(new NotifyChangeEvent(NotifyChange.Creation, NotifyChange.ActionModified, path, isdir)); + } + + /** + * Security descriptor changed notification + * + * @param path String + * @param isdir boolean + */ + public final void notifySecurityDescriptorChanged(String path, boolean isdir) + { + + // Check if file change notifications are enabled + + if (getGlobalNotifyMask() == 0 || hasSecurityDescriptorChange() == false) + return; + + // Send the change notification + + queueNotification(new NotifyChangeEvent(NotifyChange.Security, NotifyChange.ActionModified, path, isdir)); + } + + /** + * Enable debug output + * + * @param ena boolean + */ + public final void setDebug(boolean ena) + { + m_debug = ena; + } + + /** + * Shutdown the change notification processing thread + */ + public final void shutdownRequest() + { + + // Check if the processing thread is valid + + if (m_procThread != null) + { + + // Set the shutdown flag + + m_shutdown = true; + + // Wakeup the processing thread + + synchronized (m_eventList) + { + m_eventList.notifyAll(); + } + } + } + + /** + * Send buffered change notifications for a session + * + * @param req NotifyRequest + * @param evtList NotifyChangeEventList + */ + public final void sendBufferedNotifications(NotifyRequest req, NotifyChangeEventList evtList) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Send buffered notifications, req=" + req + ", evtList=" + + (evtList != null ? "" + evtList.numberOfEvents() : "null")); + + // Initialize the notification request timeout + + long tmo = System.currentTimeMillis() + NotifyRequest.DefaultRequestTimeout; + + // Allocate the NT transaction packet to send the asynchronous notification + + NTTransPacket ntpkt = new NTTransPacket(); + + // Build the change notification response SMB + + ntpkt.setParameterCount(18); + ntpkt.resetBytePointerAlign(); + + int pos = ntpkt.getPosition(); + ntpkt.setNTParameter(1, 0); // total data count + ntpkt.setNTParameter(3, pos - 4); // offset to parameter block + + // Check if the notify enum status is set + + if (req.hasNotifyEnum()) + { + + // Set the parameter block length + + ntpkt.setNTParameter(0, 0); // total parameter block count + ntpkt.setNTParameter(2, 0); // parameter block count for this packet + ntpkt.setNTParameter(6, pos - 4); // data block offset + ntpkt.setByteCount(); + + ntpkt.setCommand(PacketType.NTTransact); + + ntpkt.setFlags(SMBSrvPacket.FLG_CANONICAL + SMBSrvPacket.FLG_CASELESS); + ntpkt.setFlags2(SMBSrvPacket.FLG2_UNICODE + SMBSrvPacket.FLG2_LONGERRORCODE); + + // Set the notification request id to indicate that it has completed + + req.setCompleted(true, tmo); + req.setNotifyEnum(false); + + // Set the response for the current notify request + + ntpkt.setMultiplexId(req.getMultiplexId()); + ntpkt.setTreeId(req.getTreeId()); + ntpkt.setUserId(req.getUserId()); + ntpkt.setProcessId(req.getProcessId()); + + try + { + + // Send the response to the current session + + if (req.getSession().sendAsynchResponseSMB(ntpkt, ntpkt.getLength()) == false) + { + + // Asynchronous request was queued, clone the request packet + + ntpkt = new NTTransPacket(ntpkt); + } + } + catch (Exception ex) + { + } + } + else if (evtList != null) + { + + // Pack the change notification events + + for (int i = 0; i < evtList.numberOfEvents(); i++) + { + + // Get the current event from the list + + NotifyChangeEvent evt = evtList.getEventAt(i); + + // Get the relative file name for the event + + String relName = FileName.makeRelativePath(req.getWatchPath(), evt.getFileName()); + if (relName == null) + relName = evt.getShortFileName(); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug(" Notify evtPath=" + evt.getFileName() + ", reqPath=" + req.getWatchPath() + + ", relative=" + relName); + + // Pack the notification structure + + ntpkt.packInt(0); // offset to next structure + ntpkt.packInt(evt.getAction()); // action + ntpkt.packInt(relName.length() * 2); // file name length + ntpkt.packString(relName, true, false); + + // Check if the event is a file/directory rename, if so then add the old + // file/directory details + + if (evt.getAction() == NotifyChange.ActionRenamedNewName && evt.hasOldFileName()) + { + + // Set the offset from the first structure to this structure + + int newPos = DataPacker.longwordAlign(ntpkt.getPosition()); + DataPacker.putIntelInt(newPos - pos, ntpkt.getBuffer(), pos); + + // Get the old file name + + relName = FileName.makeRelativePath(req.getWatchPath(), evt.getOldFileName()); + if (relName == null) + relName = evt.getOldFileName(); + + // Add the old file/directory name details + + ntpkt.packInt(0); // offset to next structure + ntpkt.packInt(NotifyChange.ActionRenamedOldName); + ntpkt.packInt(relName.length() * 2); // file name length + ntpkt.packString(relName, true, false); + } + + // Calculate the parameter block length, longword align the buffer position + + int prmLen = ntpkt.getPosition() - pos; + ntpkt.alignBytePointer(); + pos = (pos + 3) & 0xFFFFFFFC; + + // Set the parameter block length + + ntpkt.setNTParameter(0, prmLen); // total parameter block count + ntpkt.setNTParameter(2, prmLen); // parameter block count for this packet + ntpkt.setNTParameter(6, ntpkt.getPosition() - 4); + // data block offset + ntpkt.setByteCount(); + + ntpkt.setCommand(PacketType.NTTransact); + + ntpkt.setFlags(SMBSrvPacket.FLG_CANONICAL + SMBSrvPacket.FLG_CASELESS); + ntpkt.setFlags2(SMBSrvPacket.FLG2_UNICODE + SMBSrvPacket.FLG2_LONGERRORCODE); + + // Set the notification request id to indicate that it has completed + + req.setCompleted(true, tmo); + + // Set the response for the current notify request + + ntpkt.setMultiplexId(req.getMultiplexId()); + ntpkt.setTreeId(req.getTreeId()); + ntpkt.setUserId(req.getUserId()); + ntpkt.setProcessId(req.getProcessId()); + + try + { + + // Send the response to the current session + + if (req.getSession().sendAsynchResponseSMB(ntpkt, ntpkt.getLength()) == false) + { + + // Asynchronous request was queued, clone the request packet + + ntpkt = new NTTransPacket(ntpkt); + } + } + catch (Exception ex) + { + } + } + } + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("sendBufferedNotifications() done"); + } + + /** + * Queue a change notification event for processing + * + * @param evt NotifyChangeEvent + */ + protected final void queueNotification(NotifyChangeEvent evt) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Queue notification event=" + evt.toString()); + + // Queue the notification event to the main notification handler thread + + synchronized (m_eventList) + { + + // Add the event to the list + + m_eventList.addEvent(evt); + + // Notify the processing thread that there are events to process + + m_eventList.notifyAll(); + } + } + + /** + * Send change notifications to sessions with notification enabled that match the change event. + * + * @param evt NotifyChangeEvent + * @return int + */ + protected final int sendChangeNotification(NotifyChangeEvent evt) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("sendChangeNotification event=" + evt); + + // Get a list of notification requests that match the type/path + + Vector reqList = findMatchingRequests(evt.getFilter(), evt.getFileName(), evt.isDirectory()); + if (reqList == null || reqList.size() == 0) + return 0; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug(" Found " + reqList.size() + " matching change listeners"); + + // Initialize the notification request timeout + + long tmo = System.currentTimeMillis() + NotifyRequest.DefaultRequestTimeout; + + // Allocate the NT transaction packet to send the asynchronous notification + + NTTransPacket ntpkt = new NTTransPacket(); + + // Send the notify response to each client in the list + + for (int i = 0; i < reqList.size(); i++) + { + + // Get the current request + + NotifyRequest req = reqList.get(i); + + // Build the change notification response SMB + + ntpkt.setParameterCount(18); + ntpkt.resetBytePointerAlign(); + + int pos = ntpkt.getPosition(); + ntpkt.setNTParameter(1, 0); // total data count + ntpkt.setNTParameter(3, pos - 4); // offset to parameter block + + // Get the relative file name for the event + + String relName = FileName.makeRelativePath(req.getWatchPath(), evt.getFileName()); + if (relName == null) + relName = evt.getShortFileName(); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug(" Notify evtPath=" + evt.getFileName() + ", reqPath=" + req.getWatchPath() + + ", relative=" + relName); + + // Pack the notification structure + + ntpkt.packInt(0); // offset to next structure + ntpkt.packInt(evt.getAction()); // action + ntpkt.packInt(relName.length() * 2); // file name length + ntpkt.packString(relName, true, false); + + // Check if the event is a file/directory rename, if so then add the old file/directory + // details + + if (evt.getAction() == NotifyChange.ActionRenamedNewName && evt.hasOldFileName()) + { + + // Set the offset from the first structure to this structure + + int newPos = DataPacker.longwordAlign(ntpkt.getPosition()); + DataPacker.putIntelInt(newPos - pos, ntpkt.getBuffer(), pos); + + // Get the old file name + + relName = FileName.makeRelativePath(req.getWatchPath(), evt.getOldFileName()); + if (relName == null) + relName = evt.getOldFileName(); + + // Add the old file/directory name details + + ntpkt.packInt(0); // offset to next structure + ntpkt.packInt(NotifyChange.ActionRenamedOldName); + ntpkt.packInt(relName.length() * 2); // file name length + ntpkt.packString(relName, true, false); + } + + // Calculate the parameter block length, longword align the buffer position + + int prmLen = ntpkt.getPosition() - pos; + ntpkt.alignBytePointer(); + pos = (pos + 3) & 0xFFFFFFFC; + + // Set the parameter block length + + ntpkt.setNTParameter(0, prmLen); // total parameter block count + ntpkt.setNTParameter(2, prmLen); // parameter block count for this packet + ntpkt.setNTParameter(6, ntpkt.getPosition() - 4); + // data block offset + ntpkt.setByteCount(); + + ntpkt.setCommand(PacketType.NTTransact); + + ntpkt.setFlags(SMBSrvPacket.FLG_CANONICAL + SMBSrvPacket.FLG_CASELESS); + ntpkt.setFlags2(SMBSrvPacket.FLG2_UNICODE + SMBSrvPacket.FLG2_LONGERRORCODE); + + // Check if the request is already complete + + if (req.isCompleted() == false) + { + + // Set the notification request id to indicate that it has completed + + req.setCompleted(true, tmo); + + // Set the response for the current notify request + + ntpkt.setMultiplexId(req.getMultiplexId()); + ntpkt.setTreeId(req.getTreeId()); + ntpkt.setUserId(req.getUserId()); + ntpkt.setProcessId(req.getProcessId()); + + // DEBUG + + // ntpkt.DumpPacket(); + + try + { + + // Send the response to the current session + + if (req.getSession().sendAsynchResponseSMB(ntpkt, ntpkt.getLength()) == false) + { + + // Asynchronous request was queued, clone the request packet + + ntpkt = new NTTransPacket(ntpkt); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + else + { + + // Buffer the event so it can be sent when the client resets the notify request + + req.addEvent(evt); + + // DEBUG + + if (logger.isDebugEnabled() && req.getSession().hasDebug(SMBSrvSession.DBG_NOTIFY)) + logger.debug("Buffered notify req=" + req + ", event=" + evt + ", sess=" + + req.getSession().getSessionId()); + } + + // Reset the notification pending flag for the session + + req.getSession().setNotifyPending(false); + + // DEBUG + + if (logger.isDebugEnabled() && req.getSession().hasDebug(SMBSrvSession.DBG_NOTIFY)) + logger + .debug("Asynch notify req=" + req + ", event=" + evt + ", sess=" + + req.getSession().getUniqueId()); + } + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("sendChangeNotification() done"); + + // Return the count of matching requests + + return reqList.size(); + } + + /** + * Find notify requests that match the type and path + * + * @param typ int + * @param path String + * @param isdir boolean + * @return Vector + */ + protected final synchronized Vector findMatchingRequests(int typ, String path, boolean isdir) + { + + // Create a vector to hold the matching requests + + Vector reqList = new Vector(); + + // Normalise the path string + + String matchPath = path.toUpperCase(); + + // Search for matching requests and remove them from the main request list + + int idx = 0; + long curTime = System.currentTimeMillis(); + + boolean removedReq = false; + + while (idx < m_notifyList.numberOfRequests()) + { + + // Get the current request + + NotifyRequest curReq = m_notifyList.getRequest(idx); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("findMatchingRequests() req=" + curReq.toString()); + + // Check if the request has expired + + if (curReq.hasExpired(curTime)) + { + + // Remove the request from the list + + m_notifyList.removeRequestAt(idx); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + { + logger.debug("Removed expired request req=" + curReq.toString()); + if (curReq.getBufferedEventList() != null) + { + NotifyChangeEventList bufList = curReq.getBufferedEventList(); + logger.debug(" Buffered events = " + bufList.numberOfEvents()); + for (int b = 0; b < bufList.numberOfEvents(); b++) + logger.debug(" " + (b + 1) + ": " + bufList.getEventAt(b)); + } + } + + // Indicate that q request has been removed from the queue, the global filter mask + // will need + // to be recalculated + + removedReq = true; + + // Restart the loop + + continue; + } + + // Check if the request matches the filter + + if (curReq.hasFilter(typ)) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug(" hasFilter typ=" + typ + ", watchTree=" + curReq.hasWatchTree() + ", watchPath=" + + curReq.getWatchPath() + ", matchPath=" + matchPath + ", isDir=" + isdir); + + // Check if the path matches or is a subdirectory and the whole tree is being + // watched + + boolean wantReq = false; + + if (matchPath.length() == 0 && curReq.hasWatchTree()) + wantReq = true; + else if (curReq.hasWatchTree() == true && matchPath.startsWith(curReq.getWatchPath()) == true) + wantReq = true; + else if (isdir == true && matchPath.compareTo(curReq.getWatchPath()) == 0) + wantReq = true; + else if (isdir == false) + { + + // Strip the file name from the path and compare + + String[] paths = FileName.splitPath(matchPath); + + if (paths != null && paths[0] != null) + { + + // Check if the directory part of the path is the directory being watched + + if (curReq.getWatchPath().equalsIgnoreCase(paths[0])) + wantReq = true; + } + } + + // Check if the request is required + + if (wantReq == true) + { + + // For all notify requests in the matching list we set the 'notify pending' + // state on the associated SMB + // session so that any socket writes on those sessions are synchronized until + // the change notification + // response has been sent. + + curReq.getSession().setNotifyPending(true); + + // Add the request to the matching list + + reqList.add(curReq); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug(" Added request to matching list"); + } + } + + // Move to the next request in the list + + idx++; + } + + // If requests were removed from the queue the global filter mask must be recalculated + + if (removedReq == true) + m_globalNotifyMask = m_notifyList.getGlobalFilter(); + + // Return the matching request list + + return reqList; + } + + /** + * Asynchronous change notification processing thread + */ + public void run() + { + + // Loop until shutdown + + while (m_shutdown == false) + { + + // Wait for some events to process + + synchronized (m_eventList) + { + try + { + m_eventList.wait(); + } + catch (InterruptedException ex) + { + } + } + + // Check if the shutdown flag has been set + + if (m_shutdown == true) + break; + + // Loop until all pending events have been processed + + while (m_eventList.numberOfEvents() > 0) + { + + // Remove the event at the head of the queue + + NotifyChangeEvent evt = null; + + synchronized (m_eventList) + { + evt = m_eventList.removeEventAt(0); + } + + // Check if the event is valid + + if (evt == null) + break; + + try + { + + // Send out change notifications to clients that match the filter/path + + int cnt = sendChangeNotification(evt); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("Change notify event=" + evt.toString() + ", clients=" + cnt); + } + catch (Throwable ex) + { + logger.error("NotifyChangeHandler thread", ex); + } + } + } + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("NotifyChangeHandler thread exit"); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/notify/NotifyRequest.java b/source/java/org/alfresco/filesys/smb/server/notify/NotifyRequest.java new file mode 100644 index 0000000000..bfaef9b2f0 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/notify/NotifyRequest.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.notify; + +import java.util.Date; + +import org.alfresco.filesys.server.filesys.DiskDeviceContext; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.NotifyChange; +import org.alfresco.filesys.smb.server.SMBSrvSession; + +/** + * Notify Change Request Details Class + */ +public class NotifyRequest +{ + + // Constants + + public final static long DefaultRequestTimeout = 10000L; // 10 seconds + + // Notify change filter + + private int m_filter; + + // Flag to indicate if sub-directories of the directory being watched will also trigger + // notifications + + private boolean m_watchTree; + + // Session that posted the notify change request + + private SMBSrvSession m_sess; + + // Directory being watched + + private NetworkFile m_watchDir; + + // Root relative path, normalised to uppercase + + private String m_watchPath; + + // Unique client request id. + // + // If the multiplex id equals -1 the request has completed and we are waiting for the request to + // be reset with a + // new multiplex id. + + private int m_mid; + private int m_tid; + private int m_pid; + private int m_uid; + + // Notifications to buffer whilst waiting for request to be reset + + private int m_maxQueueLen; + + // Disk device context that the request is associated with + + private DiskDeviceContext m_diskCtx; + + // Buffered event list + + private NotifyChangeEventList m_bufferedEvents; + + // Notify request completed flag + + private boolean m_completed; + private long m_expiresAt; + + // Flag to indicate that many file changes have occurred and a notify enum status should be + // returned + // to the client + + private boolean m_notifyEnum; + + /** + * Class constructor + * + * @param filter int + * @param watchTree boolean + * @param sess SMBSrvSession + * @param dir NetworkFile + * @param mid int + * @param tid int + * @param pid int + * @param uid int + * @param qlen int + */ + public NotifyRequest(int filter, boolean watchTree, SMBSrvSession sess, NetworkFile dir, int mid, int tid, int pid, + int uid, int qlen) + { + m_filter = filter; + m_watchTree = watchTree; + m_sess = sess; + m_watchDir = dir; + + m_mid = mid; + m_tid = tid; + m_pid = pid; + m_uid = uid; + + m_maxQueueLen = qlen; + + // Set the normalised watch path + + m_watchPath = m_watchDir.getFullName().toUpperCase(); + if (m_watchPath.length() == 0) + m_watchPath = "\\"; + else if (m_watchPath.indexOf('/') != -1) + m_watchPath.replace('/', '\\'); + } + + /** + * Get the notify change filter + * + * @return int + */ + public final int getFilter() + { + return m_filter; + } + + /** + * Determine if the request has completed + * + * @return boolean + */ + public final boolean isCompleted() + { + return m_completed; + } + + /** + * Determine if the request has expired + * + * @param curTime long + * @return boolean + */ + public final boolean hasExpired(long curTime) + { + if (isCompleted() == false) + return false; + else if (m_expiresAt < curTime) + return true; + return false; + } + + /** + * Determine if the filter has file name change notification, triggered if a file is created, + * renamed or deleted + * + * @return boolean + */ + public final boolean hasFileNameChange() + { + return hasFilter(NotifyChange.FileName); + } + + /** + * Determine if the filter has directory name change notification, triggered if a directory is + * created or deleted. + * + * @return boolean + */ + public final boolean hasDirectoryNameChange() + { + return hasFilter(NotifyChange.DirectoryName); + } + + /** + * Determine if the filter has attribute change notification + * + * @return boolean + */ + public final boolean hasAttributeChange() + { + return hasFilter(NotifyChange.Attributes); + } + + /** + * Determine if the filter has file size change notification + * + * @return boolean + */ + public final boolean hasFileSizeChange() + { + return hasFilter(NotifyChange.Size); + } + + /** + * Determine if the filter has last write time change notification + * + * @return boolean + */ + public final boolean hasFileWriteTimeChange() + { + return hasFilter(NotifyChange.LastWrite); + } + + /** + * Determine if the filter has last access time change notification + * + * @return boolean + */ + public final boolean hasFileAccessTimeChange() + { + return hasFilter(NotifyChange.LastAccess); + } + + /** + * Determine if the filter has creation time change notification + * + * @return boolean + */ + public final boolean hasFileCreateTimeChange() + { + return hasFilter(NotifyChange.Creation); + } + + /** + * Determine if the filter has the security descriptor change notification + * + * @return boolean + */ + public final boolean hasSecurityDescriptorChange() + { + return hasFilter(NotifyChange.Security); + } + + /** + * Check if the change filter has the specified flag enabled + * + * @param flag + * @return boolean + */ + public final boolean hasFilter(int flag) + { + return (m_filter & flag) != 0 ? true : false; + } + + /** + * Check if the notify enum flag is set + * + * @return boolean + */ + public final boolean hasNotifyEnum() + { + return m_notifyEnum; + } + + /** + * Determine if sub-directories of the directory being watched should also trigger notifications + * + * @return boolean + */ + public final boolean hasWatchTree() + { + return m_watchTree; + } + + /** + * Get the session that posted the notify request + * + * @return SMBSrvSession + */ + public final SMBSrvSession getSession() + { + return m_sess; + } + + /** + * Get the directory being watched + * + * @return NetworkFile + */ + public final NetworkFile getDirectory() + { + return m_watchDir; + } + + /** + * Get the normalised watch path + * + * @return String + */ + public final String getWatchPath() + { + return m_watchPath; + } + + /** + * Get the multiplex-id of the request + * + * @return int + */ + public final int getMultiplexId() + { + return m_mid; + } + + /** + * Get the tree id of the request + * + * @return int + */ + public final int getTreeId() + { + return m_tid; + } + + /** + * Get the process id of the request + * + * @return int + */ + public final int getProcessId() + { + return m_pid; + } + + /** + * Get the user id of the request + * + * @return int + */ + public final int getUserId() + { + return m_uid; + } + + /** + * Return the expiry time that a completed request must be reset by before being removed from + * the queue. + * + * @return long + */ + public final long getExpiryTime() + { + return m_expiresAt; + } + + /** + * Get the associated disk context + * + * @return DiskDeviceContext + */ + public final DiskDeviceContext getDiskContext() + { + return m_diskCtx; + } + + /** + * Return the maximum number of notifications to buffer whilst waiting for the request to be + * reset + * + * @return int + */ + public final int getMaximumQueueLength() + { + return m_maxQueueLen; + } + + /** + * Determine if there are buffered events + * + * @return boolean + */ + public final boolean hasBufferedEvents() + { + if (m_bufferedEvents != null && m_bufferedEvents.numberOfEvents() > 0) + return true; + return false; + } + + /** + * Return the buffered notification event list + * + * @return NotifyChangeEventList + */ + public final NotifyChangeEventList getBufferedEventList() + { + return m_bufferedEvents; + } + + /** + * Add a buffered notification event, to be sent when the notify request is reset by the client + * + * @param evt NotifyChangeEvent + */ + public final void addEvent(NotifyChangeEvent evt) + { + + // Check if the notify enum flag is set, if so then do not buffer any events + + if (hasNotifyEnum()) + return; + + // Check if the buffered event list has been allocated + + if (m_bufferedEvents == null) + m_bufferedEvents = new NotifyChangeEventList(); + + // Add the event if the list has not reached the maximum buffered event count + + if (m_bufferedEvents.numberOfEvents() < getMaximumQueueLength()) + { + + // Buffer the event until the client resets the notify filter + + m_bufferedEvents.addEvent(evt); + } + else + { + + // Remove all buffered events and set the notify enum flag to indicate that there + // have been many file changes + + removeAllEvents(); + setNotifyEnum(true); + } + } + + /** + * Remove all buffered events from the request + */ + public final void removeAllEvents() + { + if (m_bufferedEvents != null) + { + m_bufferedEvents.removeAllEvents(); + m_bufferedEvents = null; + } + } + + /** + * Clear the buffered event list, do not destroy the list + */ + public final void clearBufferedEvents() + { + m_bufferedEvents = null; + } + + /** + * Set/clear the notify enum flag that indicates if there have been many file changes + * + * @param ena boolean + */ + public final void setNotifyEnum(boolean ena) + { + m_notifyEnum = ena; + } + + /** + * Set the associated disk device context + * + * @param ctx DiskDeviceContext + */ + protected final void setDiskContext(DiskDeviceContext ctx) + { + m_diskCtx = ctx; + } + + /** + * Set the multiplex id for the notification + * + * @param mid int + */ + public final void setMultiplexId(int mid) + { + m_mid = mid; + } + + /** + * Set the request completed flag + * + * @param comp boolean + */ + public final void setCompleted(boolean comp) + { + m_completed = comp; + + if (comp) + m_expiresAt = System.currentTimeMillis() + DefaultRequestTimeout; + } + + /** + * Set the request completed flag and set an expiry time when the request expires + * + * @param comp boolean + * @param expire long + */ + public final void setCompleted(boolean comp, long expires) + { + m_completed = comp; + m_expiresAt = expires; + } + + /** + * Return the notify request as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + + str.append(getSession().getUniqueId()); + str.append(":"); + + if (getWatchPath().length() == 0) + str.append("Root"); + else + str.append(getWatchPath()); + str.append(":"); + + if (hasFileNameChange()) + str.append("File,"); + + if (hasDirectoryNameChange()) + str.append("Dir,"); + + if (hasAttributeChange()) + str.append("Attr,"); + + if (hasFileSizeChange()) + str.append("Size,"); + + if (hasFileWriteTimeChange()) + str.append("Write,"); + + if (hasFileAccessTimeChange()) + str.append("Access,"); + + if (hasFileCreateTimeChange()) + str.append("Create,"); + + if (hasSecurityDescriptorChange()) + str.append("Security,"); + + if (hasWatchTree()) + str.append("Tree"); + else + str.append("NoTree"); + + str.append(" MID="); + str.append(getMultiplexId()); + + str.append(" PID="); + str.append(getProcessId()); + + str.append(" TID="); + str.append(getTreeId()); + + str.append(" UID="); + str.append(getUserId()); + + if (isCompleted()) + { + str.append(",Completed,TMO="); + str.append(new Date(getExpiryTime()).toString()); + } + + str.append(",Queue="); + str.append(getMaximumQueueLength()); + if (hasBufferedEvents()) + { + str.append("/"); + str.append(getBufferedEventList().numberOfEvents()); + } + + if (hasNotifyEnum()) + str.append(",ENUM"); + + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/notify/NotifyRequestList.java b/source/java/org/alfresco/filesys/smb/server/notify/NotifyRequestList.java new file mode 100644 index 0000000000..808825b3ee --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/notify/NotifyRequestList.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.notify; + +import java.util.Vector; + +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.smb.server.SMBSrvSession; + +/** + * Notify Change Request List Class + */ +public class NotifyRequestList +{ + + // List of notify change requests + + private Vector m_requests; + + /** + * Default constructor + */ + public NotifyRequestList() + { + m_requests = new Vector(); + } + + /** + * Return the specified request + * + * @param idx int + * @return NotifyRequest + */ + public final synchronized NotifyRequest getRequest(int idx) + { + + // Range check the index + + if (idx >= m_requests.size()) + return null; + + // Return the notify request + + return (NotifyRequest) m_requests.elementAt(idx); + } + + /** + * Return the global filter mask, generated by combining all of the pending notify request + * filters + * + * @return int + */ + public final synchronized int getGlobalFilter() + { + + // Loop through all the requests + + int filter = 0; + + if (m_requests.size() > 0) + { + + // Build the global filter mask from all pending requests + + for (int i = 0; i < m_requests.size(); i++) + { + NotifyRequest req = m_requests.get(i); + filter |= req.getFilter(); + } + } + + // Return the filter mask + + return filter; + } + + /** + * Add a request to the list + * + * @param req NotifyRequest + */ + public final synchronized void addRequest(NotifyRequest req) + { + m_requests.addElement(req); + } + + /** + * Find the notify request for the matching ids + * + * @param mid int + * @param tid int + * @param uid int + * @param pid int + * @return NotifyRequest + */ + public final synchronized NotifyRequest findRequest(int mid, int tid, int uid, int pid) + { + + // Search for the required request, and remove it from the list + + for (int i = 0; i < m_requests.size(); i++) + { + + // Get the current request + + NotifyRequest curReq = (NotifyRequest) m_requests.elementAt(i); + if (curReq.getMultiplexId() == mid && curReq.getTreeId() == tid && curReq.getUserId() == uid + && curReq.getProcessId() == pid) + { + + // Return the request + + return curReq; + } + } + + // Request not found in the list + + return null; + } + + /** + * Find the notify request for the specified directory and filter + * + * @param dir NetworkFile + * @param filter int + * @param watchTree boolean + */ + public final synchronized NotifyRequest findRequest(NetworkFile dir, int filter, boolean watchTree) + { + + // Search for the required request + + for (int i = 0; i < m_requests.size(); i++) + { + + // Get the current request + + NotifyRequest curReq = (NotifyRequest) m_requests.elementAt(i); + if (curReq.getDirectory() == dir && curReq.getFilter() == filter && curReq.hasWatchTree() == watchTree) + { + + // Return the request + + return curReq; + } + } + + // Request not found in the list + + return null; + } + + /** + * Remove a request from the list + * + * @param req NotifyRequest + */ + public final synchronized NotifyRequest removeRequest(NotifyRequest req) + { + + // Search for the required request, and remove it from the list + + for (int i = 0; i < m_requests.size(); i++) + { + + // Get the current request + + NotifyRequest curReq = (NotifyRequest) m_requests.elementAt(i); + if (curReq == req) + { + + // Remove the request from the list + + m_requests.removeElementAt(i); + return curReq; + } + } + + // Request not found in the list + + return null; + } + + /** + * Remove a request from the list + * + * @param idx int + */ + public final synchronized NotifyRequest removeRequestAt(int idx) + { + + // Check if the request index is valid + + if (idx < 0 || idx >= m_requests.size()) + return null; + + // Remove the specified request + + NotifyRequest req = (NotifyRequest) m_requests.elementAt(idx); + m_requests.removeElementAt(idx); + return req; + } + + /** + * Remove all requests for the specified session + * + * @param sess SMBSrvSession + */ + public final synchronized void removeAllRequestsForSession(SMBSrvSession sess) + { + + // Search for the required requests, and remove from the list + + int idx = 0; + + while (idx < m_requests.size()) + { + + // Get the current request + + NotifyRequest curReq = (NotifyRequest) m_requests.elementAt(idx); + if (curReq.getSession() == sess) + { + + // Remove the request from the list + + m_requests.removeElementAt(idx); + } + else + idx++; + } + } + + /** + * Remove all requests for the specified session and tree connection + * + * @param sess SMBSrvSession + * @param tid int + */ + public final synchronized void removeAllRequestsForSession(SMBSrvSession sess, int tid) + { + + // Search for the required requests, and remove from the list + + int idx = 0; + + while (idx < m_requests.size()) + { + + // Get the current request + + NotifyRequest curReq = (NotifyRequest) m_requests.elementAt(idx); + if (curReq.getSession() == sess && curReq.getTreeId() == tid) + { + + // Remove the request from the list + + m_requests.removeElementAt(idx); + } + else + idx++; + } + } + + /** + * Remove all requests from the list + */ + public final synchronized void clearRequestList() + { + m_requests.removeAllElements(); + } + + /** + * Return the request list size + * + * @return int + */ + public final synchronized int numberOfRequests() + { + return m_requests.size(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/ntfs/NTFSStreamsInterface.java b/source/java/org/alfresco/filesys/smb/server/ntfs/NTFSStreamsInterface.java new file mode 100644 index 0000000000..67d42f3ed1 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/ntfs/NTFSStreamsInterface.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.ntfs; + +import java.io.IOException; + +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.filesys.TreeConnection; + +/** + * NTFS Streams Interface + *

    + * Optional interface that a DiskInterface driver can implement to provide file streams support. + */ +public interface NTFSStreamsInterface +{ + + /** + * Determine if NTFS streams are enabled + * + * @param sess SrvSession + * @param tree TreeConnection + * @return boolean + */ + public boolean hasStreamsEnabled(SrvSession sess, TreeConnection tree); + + /** + * Return stream information for the specified stream + * + * @param sess SrvSession + * @param tree TreeConnection + * @param streamInfo StreamInfo + * @return StreamInfo + * @exception IOException I/O error occurred + */ + public StreamInfo getStreamInformation(SrvSession sess, TreeConnection tree, StreamInfo streamInfo) + throws IOException; + + /** + * Return a list of the streams for the specified file + * + * @param sess SrvSession + * @param tree TreeConnection + * @param fileName String + * @return StreamInfoList + * @exception IOException I/O error occurred + */ + public StreamInfoList getStreamList(SrvSession sess, TreeConnection tree, String fileName) throws IOException; + + /** + * Rename a stream + * + * @param sess SrvSession + * @param tree TreeConnection + * @param oldName String + * @param newName String + * @param overWrite boolean + * @exception IOException + */ + public void renameStream(SrvSession sess, TreeConnection tree, String oldName, String newName, boolean overWrite) + throws IOException; +} diff --git a/source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfo.java b/source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfo.java new file mode 100644 index 0000000000..0c40972383 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfo.java @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.ntfs; + +/** + * File Stream Information Class + *

    + * Contains the details of a file stream. + */ +public class StreamInfo +{ + + // Constants + + public static final String StreamSeparator = ":"; + + // Set stream information flags + + public static final int SetStreamSize = 0x0001; + public static final int SetAllocationSize = 0x0002; + public static final int SetModifyDate = 0x0004; + public static final int SetCreationDate = 0x0008; + public static final int SetAccessDate = 0x0010; + + // File path and stream name + + private String m_path; + private String m_name; + + // Parent file id and stream id + + private int m_fid; + private int m_stid; + + // Stream size/allocation size + + private long m_size; + private long m_allocSize; + + // Stream creation, modification and access date/times + + private long m_createDate; + private long m_modifyDate; + private long m_accessDate; + + // Set stream information setter flags + + private int m_setFlags; + + /** + * Default constructor + */ + public StreamInfo() + { + } + + /** + * Constructor + * + * @param path String + */ + public StreamInfo(String path) + { + + // Parse the path to split into path and stream components + + parsePath(path); + } + + /** + * Constructor + * + * @param name String + * @param fid int + * @param stid int + */ + public StreamInfo(String name, int fid, int stid) + { + m_name = name; + m_fid = fid; + m_stid = stid; + } + + /** + * Constructor + * + * @param name String + * @param fid int + * @param stid int + * @param size long + * @param alloc long + */ + public StreamInfo(String name, int fid, int stid, long size, long alloc) + { + m_name = name; + m_fid = fid; + m_stid = stid; + m_size = size; + m_allocSize = alloc; + } + + /** + * Return the file path + * + * @return String + */ + public final String getPath() + { + return m_path; + } + + /** + * Return the stream name + * + * @return String + */ + public final String getName() + { + return m_name; + } + + /** + * Return the stream file id + * + * @return int + */ + public final int getFileId() + { + return m_fid; + } + + /** + * Return the stream id + * + * @return int + */ + public final int getStreamId() + { + return m_stid; + } + + /** + * Return the streams last access date/time. + * + * @return long + */ + public long getAccessDateTime() + { + return m_accessDate; + } + + /** + * Return the stream creation date/time. + * + * @return long + */ + public long getCreationDateTime() + { + return m_createDate; + } + + /** + * Return the modification date/time + * + * @return long + */ + public final long getModifyDateTime() + { + return m_modifyDate; + } + + /** + * Return the stream size + * + * @return long + */ + public final long getSize() + { + return m_size; + } + + /** + * Return the stream allocation size + * + * @return long + */ + public final long getAllocationSize() + { + return m_allocSize; + } + + /** + * Determine if the last access date/time is available. + * + * @return boolean + */ + public boolean hasAccessDateTime() + { + return m_accessDate == 0L ? false : true; + } + + /** + * Determine if the creation date/time details are available. + * + * @return boolean + */ + public boolean hasCreationDateTime() + { + return m_createDate == 0L ? false : true; + } + + /** + * Determine if the modify date/time details are available. + * + * @return boolean + */ + public boolean hasModifyDateTime() + { + return m_modifyDate == 0L ? false : true; + } + + /** + * Determine if the specified set stream information flags is enabled + * + * @param setFlag int + * @return boolean + */ + public final boolean hasSetFlag(int flag) + { + if ((m_setFlags & flag) != 0) + return true; + return false; + } + + /** + * Return the set stream information flags + * + * @return int + */ + public final int getSetStreamInformationFlags() + { + return m_setFlags; + } + + /** + * Set the path, if it contains the stream name the path will be split into file name and stream + * name components. + * + * @param path String + */ + public final void setPath(String path) + { + parsePath(path); + } + + /** + * Set the stream name + * + * @param name String + */ + public final void setName(String name) + { + m_name = name; + } + + /** + * Set the streams last access date/time. + * + * @param timesec long + */ + public void setAccessDateTime(long timesec) + { + + // Create the access date/time + + m_accessDate = timesec; + } + + /** + * Set the creation date/time for the stream. + * + * @param timesec long + */ + public void setCreationDateTime(long timesec) + { + + // Set the creation date/time + + m_createDate = timesec; + } + + /** + * Set the modifucation date/time for the stream. + * + * @param timesec long + */ + public void setModifyDateTime(long timesec) + { + + // Set the date/time + + m_modifyDate = timesec; + } + + /** + * Set the file id + * + * @param id int + */ + public final void setFileId(int id) + { + m_fid = id; + } + + /** + * Set the stream id + * + * @param id int + */ + public final void setStreamId(int id) + { + m_stid = id; + } + + /** + * Set the stream size + * + * @param size long + */ + public final void setSize(long size) + { + m_size = size; + } + + /** + * Set the stream allocation size + * + * @param alloc long + */ + public final void setAllocationSize(long alloc) + { + m_allocSize = alloc; + } + + /** + * Set the set stream information flags to indicated which values are to be set + * + * @param setFlags int + */ + public final void setStreamInformationFlags(int setFlags) + { + m_setFlags = setFlags; + } + + /** + * Parse a path to split into file name and stream name components + * + * @param path String + */ + protected final void parsePath(String path) + { + + // Check if the file name contains a stream name + + int pos = path.indexOf(StreamSeparator); + if (pos == -1) + { + m_path = path; + return; + } + + // Split the main file name and stream name + + m_path = path.substring(0, pos); + m_name = path.substring(pos + 1); + } + + /** + * Return the stream information as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getName()); + str.append(","); + str.append(getFileId()); + str.append(":"); + str.append(getStreamId()); + str.append(","); + str.append(getSize()); + str.append("/"); + str.append(getAllocationSize()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfoList.java b/source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfoList.java new file mode 100644 index 0000000000..2d3e5bc467 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/ntfs/StreamInfoList.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.ntfs; + +import java.util.Vector; + +/** + * Stream Information List Class + */ +public class StreamInfoList +{ + + // List of stream information objects + + private Vector m_list; + + /** + * Default constructor + */ + public StreamInfoList() + { + m_list = new Vector(); + } + + /** + * Add an item to the list + * + * @param info StreamInfo + */ + public final void addStream(StreamInfo info) + { + m_list.add(info); + } + + /** + * Return the stream details at the specified index + * + * @param idx int + * @return StreamInfo + */ + public final StreamInfo getStreamAt(int idx) + { + + // Range check the index + + if (idx < 0 || idx >= m_list.size()) + return null; + + // Return the required stream information + + return m_list.get(idx); + } + + /** + * Find a stream by name + * + * @param name String + * @return StreamInfo + */ + public final StreamInfo findStream(String name) + { + + // Search for the required stream + + for (int i = 0; i < m_list.size(); i++) + { + + // Get the current stream information + + StreamInfo sinfo = m_list.get(i); + + // Check if the stream name matches + + if (sinfo.getName().equals(name)) + return sinfo; + } + + // Stream not found + + return null; + } + + /** + * Return the count of streams in the list + * + * @return int + */ + public final int numberOfStreams() + { + return m_list.size(); + } + + /** + * Remove the specified stream from the list + * + * @param idx int + * @return StreamInfo + */ + public final StreamInfo removeStream(int idx) + { + + // Range check the index + + if (idx < 0 || idx >= m_list.size()) + return null; + + // Remove the required stream + + return m_list.remove(idx); + } + + /** + * Remove the specified stream from the list + * + * @param name String + * @return StreamInfo + */ + public final StreamInfo removeStream(String name) + { + + // Search for the required stream + + for (int i = 0; i < m_list.size(); i++) + { + + // Get the current stream information + + StreamInfo sinfo = m_list.get(i); + + // Check if the stream name matches + + if (sinfo.getName().equals(name)) + { + + // Remove the stream from the list + + m_list.removeElementAt(i); + return sinfo; + } + } + + // Stream not found + + return null; + } + + /** + * Remove all streams from the list + */ + public final void removeAllStreams() + { + m_list.removeAllElements(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java b/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java new file mode 100644 index 0000000000..f99e0ab7d9 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import java.io.FileNotFoundException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.StringTokenizer; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.server.filesys.FileAttribute; +import org.alfresco.filesys.server.filesys.FileExistsException; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Class with supplying helper methods and potentially acting as a cache for + * queries. + * + * @author derekh + */ +public class CifsHelper +{ + // Logging + private static Log logger = LogFactory.getLog(CifsHelper.class); + + // Services + private DictionaryService dictionaryService; + private NodeService nodeService; + private FileFolderService fileFolderService; + private MimetypeService mimetypeService; + private PermissionService permissionService; + + /** + * Class constructor + */ + public CifsHelper() + { + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * @param serviceRegistry for repo connection + * @param nodeRef + * @return Returns true if the node is a subtype of {@link ContentModel#TYPE_FOLDER folder} + * @throws AlfrescoRuntimeException if the type is neither related to a folder or content + */ + public boolean isDirectory(NodeRef nodeRef) + { + QName nodeTypeQName = nodeService.getType(nodeRef); + if (dictionaryService.isSubClass(nodeTypeQName, ContentModel.TYPE_FOLDER)) + { + return true; + } + else if (dictionaryService.isSubClass(nodeTypeQName, ContentModel.TYPE_CONTENT)) + { + return false; + } + else + { + // it is not a directory, but what is it? + return false; + } + } + + /** + * Extract a single node's file info, where the node is reference by + * a path relative to an ancestor node. + * + * @param pathRootNodeRef + * @param path + * @return Returns the existing node reference + * @throws FileNotFoundException + */ + public FileInfo getFileInformation(NodeRef pathRootNodeRef, String path) throws FileNotFoundException + { + // get the node being referenced + NodeRef nodeRef = getNodeRef(pathRootNodeRef, path); + + FileInfo fileInfo = getFileInformation(nodeRef); + + return fileInfo; + } + + /** + * Helper method to extract file info from a specific node. + *

    + * This method goes direct to the repo for all information and no data is + * cached here. + * + * @param nodeRef the node that the path is relative to + * @param path the path to get info for + * @return Returns the file information pertinent to the node + * @throws FileNotFoundException if the path refers to a non-existent file + */ + public FileInfo getFileInformation(NodeRef nodeRef) throws FileNotFoundException + { + // get the file info + org.alfresco.service.cmr.model.FileInfo fileFolderInfo = fileFolderService.getFileInfo(nodeRef); + + // retrieve required properties and create file info + FileInfo fileInfo = new FileInfo(); + + // unset all attribute flags + int fileAttributes = 0; + fileInfo.setFileAttributes(fileAttributes); + + if (fileFolderInfo.isFolder()) + { + // add directory attribute + fileAttributes |= FileAttribute.Directory; + fileInfo.setFileAttributes(fileAttributes); + } + else + { + Map nodeProperties = fileFolderInfo.getProperties(); + // get the file size + ContentData contentData = (ContentData) nodeProperties.get(ContentModel.PROP_CONTENT); + long size = 0L; + if (contentData != null) + { + size = contentData.getSize(); + } + fileInfo.setSize(size); + + // Set the allocation size by rounding up the size to a 512 byte block boundary + if ( size > 0) + fileInfo.setAllocationSize((size + 512L) & 0xFFFFFFFFFFFFFE00L); + } + + // created + Date createdDate = fileFolderInfo.getCreatedDate(); + if (createdDate != null) + { + long created = DefaultTypeConverter.INSTANCE.longValue(createdDate); + fileInfo.setCreationDateTime(created); + } + // modified + Date modifiedDate = fileFolderInfo.getModifiedDate(); + if (modifiedDate != null) + { + long modified = DefaultTypeConverter.INSTANCE.longValue(modifiedDate); + fileInfo.setModifyDateTime(modified); + } + // name + String name = fileFolderInfo.getName(); + if (name != null) + { + fileInfo.setFileName(name); + } + + // read/write access + if ( permissionService.hasPermission(nodeRef, PermissionService.WRITE) == AccessStatus.DENIED) + fileInfo.setFileAttributes(fileInfo.getFileAttributes() + FileAttribute.ReadOnly); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Fetched file info: \n" + + " info: " + fileInfo); + } + return fileInfo; + } + + /** + * Creates a file or directory using the given paths. + *

    + * If the directory path doesn't exist, then all the parent directories will be created. + * If the file path is null, then the file will not be created + * + * @param rootNodeRef the root node of the path + * @param path the path to a node + * @param isFile true if the node to be created must be a file + * @return Returns a newly created file or folder node + * @throws FileExistsException if the file or folder already exists + */ + public NodeRef createNode(NodeRef rootNodeRef, String path, boolean isFile) throws FileExistsException + { + // split the path up into its constituents + StringTokenizer tokenizer = new StringTokenizer(path, FileName.DOS_SEPERATOR_STR, false); + List folderPathElements = new ArrayList(10); + String name = null; + while (tokenizer.hasMoreTokens()) + { + String pathElement = tokenizer.nextToken(); + + if (!tokenizer.hasMoreTokens()) + { + // the last token becomes the name + name = pathElement; + } + else + { + // add the path element to the parent folder path + folderPathElements.add(pathElement); + } + } + // ensure that the folder path exists + NodeRef parentFolderNodeRef = rootNodeRef; + if (folderPathElements.size() > 0) + { + parentFolderNodeRef = fileFolderService.makeFolders( + rootNodeRef, + folderPathElements, + ContentModel.TYPE_FOLDER).getNodeRef(); + } + // add the file or folder + QName typeQName = isFile ? ContentModel.TYPE_CONTENT : ContentModel.TYPE_FOLDER; + try + { + NodeRef nodeRef = fileFolderService.create(parentFolderNodeRef, name, typeQName).getNodeRef(); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created node: \n" + + " device root: " + rootNodeRef + "\n" + + " path: " + path + "\n" + + " is file: " + isFile + "\n" + + " new node: " + nodeRef); + } + return nodeRef; + } + catch (org.alfresco.service.cmr.model.FileExistsException e) + { + throw new FileExistsException(path); + } + } + + private void addDescendents(List pathRootNodeRefs, Stack pathElements, List results) + { + if (pathElements.isEmpty()) + { + // if this method is called with an empty path element stack, then the + // current context nodes are the results to be added + results.addAll(pathRootNodeRefs); + return; + } + + // take the first path element off the stack + String pathElement = pathElements.pop(); + + // iterate over each path root node + for (NodeRef pathRootNodeRef : pathRootNodeRefs) + { + // deal with cyclic relationships by not traversing down any node already in the results + if (results.contains(pathRootNodeRef)) + { + continue; + } + // get direct descendents along the path + List directDescendents = getDirectDescendents(pathRootNodeRef, pathElement); + // recurse onto the descendents + addDescendents(directDescendents, pathElements, results); + } + + // restore the path element stack + pathElements.push(pathElement); + } + + /** + * Performs an XPath query to get the first-level descendents matching the given path + * + * @param pathRootNodeRef + * @param pathElement + * @return + */ + private List getDirectDescendents(NodeRef pathRootNodeRef, String pathElement) + { + if (logger.isDebugEnabled()) + { + logger.debug("Getting direct descendents: \n" + + " Path Root: " + pathRootNodeRef + "\n" + + " Path Element: " + pathElement); + } + + // do the lookup + List childInfos = fileFolderService.search(pathRootNodeRef, pathElement, false); + // convert to noderefs + List results = new ArrayList(childInfos.size()); + for (org.alfresco.service.cmr.model.FileInfo info : childInfos) + { + results.add(info.getNodeRef()); + } + // done + return results; + } + + /** + * Finds the nodes being reference by the given directory and file paths. + *

    + * Examples of the path are: + *

      + *
    • \New Folder\New Text Document.txt
    • + *
    • \New Folder\Sub Folder
    • + *
    + * + * @param searchRootNodeRef the node from which to start the path search + * @param path the search path to either a folder or file + * @return Returns references to all matching nodes + */ + public List getNodeRefs(NodeRef pathRootNodeRef, String path) + { + // tokenize the path and push into a stack in reverse order so that + // the root directory gets popped first + StringTokenizer tokenizer = new StringTokenizer(path, FileName.DOS_SEPERATOR_STR, false); + String[] tokens = new String[tokenizer.countTokens()]; + int count = 0; + while(tokenizer.hasMoreTokens()) + { + tokens[count] = tokenizer.nextToken(); + count++; + } + Stack pathElements = new Stack(); + for (int i = tokens.length - 1; i >= 0; i--) + { + pathElements.push(tokens[i]); + } + + // start with a single parent node + List pathRootNodeRefs = Collections.singletonList(pathRootNodeRef); + + // result storage + List results = new ArrayList(5); + + // kick off the path walking + addDescendents(pathRootNodeRefs, pathElements, results); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved node references for path: \n" + + " path root: " + pathRootNodeRef + "\n" + + " path: " + path + "\n" + + " results: " + results); + } + return results; + } + + /** + * Attempts to fetch a specific single node at the given path. + * + * @throws FileNotFoundException if the path can't be resolved to a node + * + * @see #getNodeRefs(NodeRef, String) + */ + public NodeRef getNodeRef(NodeRef pathRootNodeRef, String path) throws FileNotFoundException + { + // attempt to get the file/folder node using hierarchy walking + List nodeRefs = getNodeRefs(pathRootNodeRef, path); + if (nodeRefs.size() == 0) + { + throw new FileNotFoundException(path); + } + else if (nodeRefs.size() > 1) + { + logger.warn("Multiple matching nodes: \n" + + " search root: " + pathRootNodeRef + "\n" + + " path: " + path); + } + // take the first one - not sure if it is possible for the path to refer to more than one + NodeRef nodeRef = nodeRefs.get(0); + // done + return nodeRef; + } + + /** + * Relink the content data from a new node to an existing node to preserve the version history. + * + * @param oldNodeRef NodeRef + * @param newNodeRef NodeRef + */ + public void relinkNode(NodeRef tempNodeRef, NodeRef nodeToMoveRef, NodeRef newParentNodeRef, String newName) + throws FileNotFoundException, FileExistsException + { + // Get the properties for the old and new nodes + org.alfresco.service.cmr.model.FileInfo tempFileInfo = fileFolderService.getFileInfo(tempNodeRef); + org.alfresco.service.cmr.model.FileInfo fileToMoveInfo = fileFolderService.getFileInfo(nodeToMoveRef); + + // Save the current name of the old node + String tempName = tempFileInfo.getName(); + + try + { + // rename temp file to the new name + fileFolderService.rename(tempNodeRef, newName); + // rename new file to old name + fileFolderService.rename(nodeToMoveRef, tempName); + } + catch (org.alfresco.service.cmr.model.FileNotFoundException e) + { + throw new FileNotFoundException(e.getMessage()); + } + catch (org.alfresco.service.cmr.model.FileExistsException e) + { + throw new FileExistsException(e.getMessage()); + } + + if (!tempFileInfo.isFolder() && !fileToMoveInfo.isFolder()) + { + // swap the content between the two + ContentData oldContentData = tempFileInfo.getContentData(); + if (oldContentData == null) + { + String mimetype = mimetypeService.guessMimetype(tempName); + oldContentData = ContentData.setMimetype(null, mimetype); + } + ContentData newContentData = fileToMoveInfo.getContentData(); + if (newContentData == null) + { + String mimetype = mimetypeService.guessMimetype(newName); + newContentData = ContentData.setMimetype(null, mimetype); + } + + nodeService.setProperty(tempNodeRef, ContentModel.PROP_CONTENT, newContentData); + nodeService.setProperty(nodeToMoveRef, ContentModel.PROP_CONTENT, oldContentData); + } + } + + public void move(NodeRef nodeToMoveRef, NodeRef newParentNodeRef, String newName) throws FileExistsException + { + try + { + fileFolderService.move(nodeToMoveRef, newParentNodeRef, newName); + } + catch (org.alfresco.service.cmr.model.FileExistsException e) + { + throw new FileExistsException(newName); + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException("Move failed: \n" + + " node to move: " + nodeToMoveRef + "\n" + + " new parent: " + newParentNodeRef + "\n" + + " new name: " + newName, + e); + } + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/CifsIntegrationTest.java b/source/java/org/alfresco/filesys/smb/server/repo/CifsIntegrationTest.java new file mode 100644 index 0000000000..12b7260299 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/CifsIntegrationTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import org.alfresco.filesys.CIFSServer; +import org.alfresco.filesys.server.filesys.DiskSharedDevice; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.util.BaseAlfrescoTestCase; + +/** + * Checks that the required configuration details are obtainable from the CIFS components. + * + * @author Derek Hulley + */ +public class CifsIntegrationTest extends BaseAlfrescoTestCase +{ + + public void testGetServerName() + { + CIFSServer cifsServer = (CIFSServer) ctx.getBean("cifsServer"); + assertNotNull("No CIFS server available", cifsServer); + // the server might, quite legitimately, not start + if (!cifsServer.isStarted()) + { + return; + } + + // get the server name + String serverName = cifsServer.getConfiguration().getServerName(); + assertNotNull("No server name available", serverName); + assertTrue("No server name available (zero length)", serverName.length() > 0); + + // Get the primary filesystem, might be null if the home folder mapper is configured + + DiskSharedDevice mainFilesys = cifsServer.getConfiguration().getPrimaryFilesystem(); + + if ( mainFilesys != null) + { + // Check the share name + + String shareName = mainFilesys.getName(); + assertNotNull("No share name available", shareName); + assertTrue("No share name available (zero length)", shareName.length() > 0); + + // Check that the context is valid + + ContentContext filesysCtx = (ContentContext) mainFilesys.getContext(); + assertNotNull("Content context is null", filesysCtx); + assertNotNull("Store id is null", filesysCtx.getStoreName()); + assertNotNull("Root path is null", filesysCtx.getRootPath()); + assertNotNull("Root node is null", filesysCtx.getRootNode()); + + // Check the root node + + NodeService nodeService = (NodeService) ctx.getBean(ServiceRegistry.NODE_SERVICE.getLocalName()); + // get the share root node and check that it exists + NodeRef shareNodeRef = filesysCtx.getRootNode(); + assertNotNull("No share root node available", shareNodeRef); + assertTrue("Share root node doesn't exist", nodeService.exists(shareNodeRef)); + } + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentContext.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentContext.java new file mode 100644 index 0000000000..e8e91f614b --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentContext.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import org.alfresco.filesys.server.filesys.*; +import org.alfresco.service.cmr.repository.*; + +/** + * Content Filesystem Context Class + * + *

    Contains per filesystem context. + * + * @author GKSpencer + */ +public class ContentContext extends DiskDeviceContext +{ + // Store and root path + + private String m_storeName; + private String m_rootPath; + + // Root node + + private NodeRef m_rootNodeRef; + + // File state table + + private FileStateTable m_stateTable; + + /** + * Class constructor + * + * @param storeName String + * @param rootPath String + * @param rootNodeRef NodeRef + */ + public ContentContext(String storeName, String rootPath, NodeRef rootNodeRef) + { + super(rootNodeRef.toString()); + + m_storeName = storeName; + m_rootPath = rootPath; + + m_rootNodeRef = rootNodeRef; + + // Create the file state table + + m_stateTable = new FileStateTable(); + } + + /** + * Return the filesystem type, either FileSystem.TypeFAT or FileSystem.TypeNTFS. + * + * @return String + */ + public String getFilesystemType() + { + return FileSystem.TypeNTFS; + } + + /** + * Return the store name + * + * @return String + */ + public final String getStoreName() + { + return m_storeName; + } + + /** + * Return the root path + * + * @return String + */ + public final String getRootPath() + { + return m_rootPath; + } + + /** + * Return the root node + * + * @return NodeRef + */ + public final NodeRef getRootNode() + { + return m_rootNodeRef; + } + + /** + * Determine if the file state table is enabled + * + * @return boolean + */ + public final boolean hasStateTable() + { + return m_stateTable != null ? true : false; + } + + /** + * Return the file state table + * + * @return FileStateTable + */ + public final FileStateTable getStateTable() + { + return m_stateTable; + } + + /** + * Enable/disable the file state table + * + * @param ena boolean + */ + public final void enableStateTable(boolean ena) + { + if ( ena == false) + m_stateTable = null; + else if ( m_stateTable == null) + m_stateTable = new FileStateTable(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java new file mode 100644 index 0000000000..167fffeb9a --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java @@ -0,0 +1,1519 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +import javax.transaction.UserTransaction; + +import org.alfresco.config.ConfigElement; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.core.DeviceContext; +import org.alfresco.filesys.server.core.DeviceContextException; +import org.alfresco.filesys.server.filesys.AccessDeniedException; +import org.alfresco.filesys.server.filesys.AccessMode; +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.FileSharingException; +import org.alfresco.filesys.server.filesys.FileStatus; +import org.alfresco.filesys.server.filesys.FileSystem; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.filesys.server.filesys.SrvDiskInfo; +import org.alfresco.filesys.server.filesys.TreeConnection; +import org.alfresco.filesys.smb.SharingMode; +import org.alfresco.filesys.smb.server.repo.FileState.FileStateStatus; +import org.alfresco.service.cmr.lock.NodeLockedException; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Content repository filesystem driver class + * + *

    Provides a filesystem interface for various protocols such as SMB/CIFS and FTP. + * + * @author Derek Hulley + */ +public class ContentDiskDriver implements DiskInterface +{ + private static final String KEY_STORE = "store"; + private static final String KEY_ROOT_PATH = "rootPath"; + + private static final Log logger = LogFactory.getLog(ContentDiskDriver.class); + + private CifsHelper cifsHelper; + private TransactionService transactionService; + private NamespaceService namespaceService; + private NodeService nodeService; + private NodeService unprotectedNodeService; + private SearchService unprotectedSearchService; + private ContentService contentService; + private PermissionService permissionService; + + /** + * Class constructor + * + * @param serviceRegistry to connect to the repository services + */ + public ContentDiskDriver(CifsHelper cifsHelper) + { + this.cifsHelper = cifsHelper; + } + + /** + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param nodeService the node service + */ + public void setUnprotectedNodeService(NodeService nodeService) + { + this.unprotectedNodeService = nodeService; + } + + /** + * @param searchService the search service + */ + public void setUnprotectedSearchService(SearchService searchService) + { + this.unprotectedSearchService = searchService; + } + + + /** + * @param transactionService the transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the permission service + * + * @param permissionService PermissionService + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * Parse and validate the parameter string and create a device context object for this instance + * of the shared device. The same DeviceInterface implementation may be used for multiple + * shares. + * + * @param args ConfigElement + * @return DeviceContext + * @exception DeviceContextException + */ + public DeviceContext createContext(ConfigElement cfg) throws DeviceContextException + { + // Wrap the initialization in a transaction + + UserTransaction tx = transactionService.getUserTransaction(true); + + ContentContext context = null; + + try + { + // Start the transaction + + if ( tx != null) + tx.begin(); + + // Get the store + + ConfigElement storeElement = cfg.getChild(KEY_STORE); + if (storeElement == null || storeElement.getValue() == null || storeElement.getValue().length() == 0) + { + throw new DeviceContextException("Device missing init value: " + KEY_STORE); + } + String storeValue = storeElement.getValue(); + StoreRef storeRef = new StoreRef(storeValue); + + // Connect to the repo and ensure that the store exists + + if (!unprotectedNodeService.exists(storeRef)) + { + throw new DeviceContextException("Store not created prior to application startup: " + storeRef); + } + NodeRef storeRootNodeRef = unprotectedNodeService.getRootNode(storeRef); + + // Get the root path + + ConfigElement rootPathElement = cfg.getChild(KEY_ROOT_PATH); + if (rootPathElement == null || rootPathElement.getValue() == null || rootPathElement.getValue().length() == 0) + { + throw new DeviceContextException("Device missing init value: " + KEY_ROOT_PATH); + } + String rootPath = rootPathElement.getValue(); + + // Find the root node for this device + + List nodeRefs = unprotectedSearchService.selectNodes( + storeRootNodeRef, rootPath, null, namespaceService, false); + + NodeRef rootNodeRef = null; + + if (nodeRefs.size() > 1) + { + throw new DeviceContextException("Multiple possible roots for device: \n" + + " root path: " + rootPath + "\n" + + " results: " + nodeRefs); + } + else if (nodeRefs.size() == 0) + { + // nothing found + throw new DeviceContextException("No root found for device: \n" + + " root path: " + rootPath); + } + else + { + // we found a node + rootNodeRef = nodeRefs.get(0); + } + + // Commit the transaction + + tx.commit(); + tx = null; + + // Create the context + + context = new ContentContext(storeValue, rootPath, rootNodeRef); + + // Default the filesystem to look like an 80Gb sized disk with 90% free space + + context.setDiskInformation(new SrvDiskInfo(2560, 64, 512, 2304)); + + // Set parameters + + context.setFilesystemAttributes(FileSystem.CasePreservedNames); + } + catch (Exception ex) + { + logger.error("Error during create context", ex); + } + finally + { + // If there is an active transaction then roll it back + + if ( tx != null) + { + try + { + tx.rollback(); + } + catch (Exception ex) + { + logger.warn("Failed to rollback transaction", ex); + } + } + } + + // Return the context for this shared filesystem + + return context; + } + + /** + * Determine if the disk device is read-only. + * + * @param sess Server session + * @param ctx Device context + * @return boolean + * @exception java.io.IOException If an error occurs. + */ + public boolean isReadOnly(SrvSession sess, DeviceContext ctx) throws IOException + { + return false; + } + + /** + * Get the file information for the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param name File name/path that information is required for. + * @return File information if valid, else null + * @exception java.io.IOException The exception description. + */ + public FileInfo getFileInformation(SrvSession session, TreeConnection tree, String path) throws IOException + { + // get the device root + + ContentContext ctx = (ContentContext) tree.getContext(); + NodeRef infoParentNodeRef = ctx.getRootNode(); + String infoPath = path; + + try + { + // Get the node ref for the path, chances are there is a file state in the cache + + FileInfo finfo = null; + NodeRef nodeRef = getNodeForPath(tree, infoPath); + if ( nodeRef != null) + { + // Get the file information for the node + + finfo = cifsHelper.getFileInformation(nodeRef); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("getInfo using cached noderef for path " + path); + } + + // If the required node was not in the state cache, the parent folder node might be + + session.beginTransaction(transactionService, true); + + if ( finfo == null) + { + String[] paths = FileName.splitPath(path); + if ( paths[0] != null && paths[0].length() > 1) + { + // Find the node ref for the folder being searched + + nodeRef = getNodeForPath(tree, paths[0]); + + if ( nodeRef != null) + { + infoParentNodeRef = nodeRef; + infoPath = paths[1]; + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("getInfo using cached noderef for parent " + path); + } + } + + // Access the repository to get the file information + + finfo = cifsHelper.getFileInformation(infoParentNodeRef, infoPath); + + // DEBUG + if (logger.isDebugEnabled()) + { + logger.debug("Getting file information: \n" + + " path: " + path + "\n" + + " file info: " + finfo); + } + } + + // Return the file information + return finfo; + } + catch (FileNotFoundException e) + { + // a valid use case + if (logger.isDebugEnabled()) + { + logger.debug("Getting file information - File not found: \n" + + " path: " + path); + } + throw e; + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Get file info - access denied, " + path); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Get file information " + path); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Get file info error", ex); + + // Convert to a general I/O exception + + throw new IOException("Get file information " + path); + } + } + + /** + * Start a new search on the filesystem using the specified searchPath that may contain + * wildcards. + * + * @param sess Server session + * @param tree Tree connection + * @param searchPath File(s) to search for, may include wildcards. + * @param attrib Attributes of the file(s) to search for, see class SMBFileAttribute. + * @return SearchContext + * @exception java.io.FileNotFoundException If the search could not be started. + */ + public SearchContext startSearch(SrvSession sess, TreeConnection tree, String searchPath, int attributes) throws FileNotFoundException + { + try + { + // Access the device context + + ContentContext ctx = (ContentContext) tree.getContext(); + + String searchFileSpec = searchPath; + NodeRef searchRootNodeRef = ctx.getRootNode(); + + // Create the transaction + + sess.beginTransaction(transactionService, true); + + // If the state table is available see if we can speed up the search using either cached + // file information or find the folder node to be searched without having to walk the path + + if ( ctx.hasStateTable()) + { + // See if the folder to be searched has a file state, we can avoid having to walk the path + + String[] paths = FileName.splitPath(searchPath); + if ( paths[0] != null && paths[0].length() > 1) + { + // Find the node ref for the folder being searched + + NodeRef nodeRef = getNodeForPath(tree, paths[0]); + + if ( nodeRef != null) + { + searchRootNodeRef = nodeRef; + searchFileSpec = paths[1]; + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Search using cached noderef for path " + searchPath); + } + } + } + + // Start the search + + SearchContext searchCtx = ContentSearchContext.search(cifsHelper, searchRootNodeRef, + searchFileSpec, attributes); + + // done + + if (logger.isDebugEnabled()) + { + logger.debug("Started search: \n" + + " search path: " + searchPath + "\n" + + " attributes: " + attributes); + } + return searchCtx; + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Start search - access denied, " + searchPath); + + // Convert to a file not found status + + throw new FileNotFoundException("Start search " + searchPath); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Start search", ex); + + // Convert to a file not found status + + throw new FileNotFoundException("Start search " + searchPath); + } + } + + /** + * Check if the specified file exists, and whether it is a file or directory. + * + * @param sess Server session + * @param tree Tree connection + * @param name java.lang.String + * @return int + * @see FileStatus + */ + public int fileExists(SrvSession sess, TreeConnection tree, String name) + { + + int status = FileStatus.Unknown; + + try + { + // Check for a cached file state + + ContentContext ctx = (ContentContext) tree.getContext(); + FileState fstate = null; + + if ( ctx.hasStateTable()) + ctx.getStateTable().findFileState(name); + + if ( fstate != null) + { + FileStateStatus fsts = fstate.getFileStatus(); + + if ( fsts == FileStateStatus.FileExists) + status = FileStatus.FileExists; + else if ( fsts == FileStateStatus.FolderExists) + status = FileStatus.DirectoryExists; + else if ( fsts == FileStateStatus.NotExist || fsts == FileStateStatus.Renamed) + status = FileStatus.NotExist; + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Cache hit - fileExists() " + name + ", sts=" + status); + } + else + { + // Create the transaction + + sess.beginTransaction(transactionService, true); + + // Get the file information to check if the file/folder exists + + FileInfo info = getFileInformation(sess, tree, name); + if (info.isDirectory()) + { + status = FileStatus.DirectoryExists; + } + else + { + status = FileStatus.FileExists; + } + } + } + catch (FileNotFoundException e) + { + status = FileStatus.NotExist; + } + catch (IOException e) + { + // Debug + + logger.debug("File exists error, " + name, e); + + status = FileStatus.NotExist; + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("File status determined: \n" + + " name: " + name + "\n" + + " status: " + status); + } + return status; + } + + /** + * Open a file or folder + * + * @param sess SrvSession + * @param tree TreeConnection + * @param params FileOpenParams + * @return NetworkFile + * @exception IOException + */ + public NetworkFile openFile(SrvSession sess, TreeConnection tree, FileOpenParams params) throws IOException + { + // Create the transaction + + sess.beginTransaction(transactionService, false); + + try + { + // Get the node for the path + + ContentContext ctx = (ContentContext) tree.getContext(); + NodeRef nodeRef = getNodeForPath(tree, params.getPath()); + + // Check permissions on the file/folder node + // + // Check for read access + + if ( params.hasAccessMode(AccessMode.NTRead) && + permissionService.hasPermission(nodeRef, PermissionService.READ) == AccessStatus.DENIED) + throw new AccessDeniedException("No read access to " + params.getFullPath()); + + // Check for write access + + if ( params.hasAccessMode(AccessMode.NTWrite) && + permissionService.hasPermission(nodeRef, PermissionService.WRITE) == AccessStatus.DENIED) + throw new AccessDeniedException("No write access to " + params.getFullPath()); + + // Check for delete access + + if ( params.hasAccessMode(AccessMode.NTDelete) && + permissionService.hasPermission(nodeRef, PermissionService.DELETE) == AccessStatus.DENIED) + throw new AccessDeniedException("No delete access to " + params.getFullPath()); + + // Check if there is a file state for the file + + FileState fstate = null; + + if ( ctx.hasStateTable()) + { + // Check if there is a file state for the file + + fstate = ctx.getStateTable().findFileState( params.getPath()); + + if ( fstate != null) + { + // Check if the file exists + + if ( fstate.exists() == false) + throw new FileNotFoundException(); + + // Check if the open request shared access indicates exclusive file access + + if ( fstate != null && params.getSharedAccess() == SharingMode.NOSHARING && + fstate.getOpenCount() > 0) + throw new FileSharingException("File already open, " + params.getPath()); + } + } + + // Create the network file + + NetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, cifsHelper, nodeRef, params); + + // Create a file state for the open file + + if ( ctx.hasStateTable()) + { + if ( fstate == null) + fstate = ctx.getStateTable().findFileState(params.getPath(), params.isDirectory(), true); + + // Update the file state, cache the node + + fstate.incrementOpenCount(); + fstate.setNodeRef(nodeRef); + } + + // Debug + + if (logger.isDebugEnabled()) + { + logger.debug("Opened network file: \n" + + " path: " + params.getPath() + "\n" + + " file open parameters: " + params + "\n" + + " network file: " + netFile); + } + + // Return the network file + + return netFile; + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Open file - access denied, " + params.getFullPath()); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Open file " + params.getFullPath()); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Open file error", ex); + + // Convert to a general I/O exception + + throw new IOException("Open file " + params.getFullPath()); + } + } + + /** + * Create a new file on the file system. + * + * @param sess Server session + * @param tree Tree connection + * @param params File create parameters + * @return NetworkFile + * @exception java.io.IOException If an error occurs. + */ + public NetworkFile createFile(SrvSession sess, TreeConnection tree, FileOpenParams params) throws IOException + { + // Create the transaction + + sess.beginTransaction(transactionService, false); + + try + { + // get the device root + + ContentContext ctx = (ContentContext) tree.getContext(); + NodeRef deviceRootNodeRef = ctx.getRootNode(); + + String path = params.getPath(); + + // If the state table is available then try to find the parent folder node for the new file + // to save having to walk the path + + if ( ctx.hasStateTable()) + { + // See if the parent folder has a file state, we can avoid having to walk the path + + String[] paths = FileName.splitPath(path); + if ( paths[0] != null && paths[0].length() > 1) + { + // Find the node ref for the folder being searched + + NodeRef nodeRef = getNodeForPath(tree, paths[0]); + + if ( nodeRef != null) + { + deviceRootNodeRef = nodeRef; + path = paths[1]; + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Create file using cached noderef for path " + paths[0]); + } + } + } + + // Create it - the path will be created, if necessary + + NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, true); + + // create the network file + NetworkFile netFile = ContentNetworkFile.createFile(nodeService, contentService, cifsHelper, nodeRef, params); + + // Add a file state for the new file/folder + + if ( ctx.hasStateTable()) + { + FileState fstate = ctx.getStateTable().findFileState(path, false, true); + if ( fstate != null) + { + // Indicate that the file is open + + fstate.setFileStatus(FileStateStatus.FileExists); + fstate.incrementOpenCount(); + fstate.setNodeRef(nodeRef); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Creaste file, state=" + fstate); + } + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created file: \n" + + " path: " + path + "\n" + + " file open parameters: " + params + "\n" + + " node: " + nodeRef + "\n" + + " network file: " + netFile); + } + return netFile; + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Create file - access denied, " + params.getFullPath()); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Create file " + params.getFullPath()); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Create file error", ex); + + // Convert to a general I/O exception + + throw new IOException("Create file " + params.getFullPath()); + } + + } + + /** + * Create a new directory on this file system. + * + * @param sess Server session + * @param tree Tree connection. + * @param params Directory create parameters + * @exception java.io.IOException If an error occurs. + */ + public void createDirectory(SrvSession sess, TreeConnection tree, FileOpenParams params) throws IOException + { + // Create the transaction + + sess.beginTransaction(transactionService, false); + + try + { + // get the device root + + ContentContext ctx = (ContentContext) tree.getContext(); + NodeRef deviceRootNodeRef = ctx.getRootNode(); + + String path = params.getPath(); + + // If the state table is available then try to find the parent folder node for the new folder + // to save having to walk the path + + if ( ctx.hasStateTable()) + { + // See if the parent folder has a file state, we can avoid having to walk the path + + String[] paths = FileName.splitPath(path); + if ( paths[0] != null && paths[0].length() > 1) + { + // Find the node ref for the folder being searched + + NodeRef nodeRef = getNodeForPath(tree, paths[0]); + + if ( nodeRef != null) + { + deviceRootNodeRef = nodeRef; + path = paths[1]; + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Create file using cached noderef for path " + paths[0]); + } + } + } + + // Create it - the path will be created, if necessary + + NodeRef nodeRef = cifsHelper.createNode(deviceRootNodeRef, path, false); + + // Add a file state for the new folder + + if ( ctx.hasStateTable()) + { + FileState fstate = ctx.getStateTable().findFileState(path, true, true); + if ( fstate != null) + { + // Indicate that the file is open + + fstate.setFileStatus(FileStateStatus.FolderExists); + fstate.incrementOpenCount(); + fstate.setNodeRef(nodeRef); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Creaste folder, state=" + fstate); + } + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created directory: \n" + + " path: " + path + "\n" + + " file open params: " + params + "\n" + + " node: " + nodeRef); + } + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Create directory - access denied, " + params.getFullPath()); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Create directory " + params.getFullPath()); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Create directory error", ex); + + // Convert to a general I/O exception + + throw new IOException("Create directory " + params.getFullPath()); + } + } + + /** + * Delete the directory from the filesystem. + * + * @param sess Server session + * @param tree Tree connection + * @param dir Directory name. + * @exception java.io.IOException The exception description. + */ + public void deleteDirectory(SrvSession sess, TreeConnection tree, String dir) throws IOException + { + // Create the transaction + + sess.beginTransaction(transactionService, false); + + // get the device root + + ContentContext ctx = (ContentContext) tree.getContext(); + NodeRef deviceRootNodeRef = ctx.getRootNode(); + + try + { + // get the node + NodeRef nodeRef = cifsHelper.getNodeRef(deviceRootNodeRef, dir); + if (nodeService.exists(nodeRef)) + { + nodeService.deleteNode(nodeRef); + + // Remove the file state + + if ( ctx.hasStateTable()) + ctx.getStateTable().removeFileState(dir); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Deleted directory: \n" + + " directory: " + dir + "\n" + + " node: " + nodeRef); + } + } + catch (FileNotFoundException e) + { + // already gone + if (logger.isDebugEnabled()) + { + logger.debug("Deleted directory : \n" + + " directory: " + dir); + } + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Delete directory - access denied, " + dir); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Delete directory " + dir); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Delete directory", ex); + + // Convert to a general I/O exception + + throw new IOException("Delete directory " + dir); + } + } + + /** + * Flush any buffered output for the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file context. + * @exception java.io.IOException The exception description. + */ + public void flushFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws IOException + { + // Flush the file data + + file.flushFile(); + } + + /** + * Close the file. + * + * @param sess Server session + * @param tree Tree connection. + * @param param Network file context. + * @exception java.io.IOException If an error occurs. + */ + public void closeFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws IOException + { + // Create the transaction + + sess.beginTransaction(transactionService, false); + + // Get the associated file state + + ContentContext ctx = (ContentContext) tree.getContext(); + + if ( ctx.hasStateTable()) + { + FileState fstate = ctx.getStateTable().findFileState(file.getFullName()); + if ( fstate != null) + fstate.decrementOpenCount(); + } + + // Defer to the network file to close the stream and remove the content + + file.closeFile(); + + // remove the node if necessary + if (file.hasDeleteOnClose()) + { + ContentNetworkFile contentNetFile = (ContentNetworkFile) file; + NodeRef nodeRef = contentNetFile.getNodeRef(); + // we don't know how long the network file has had the reference, so check for existence + if (nodeService.exists(nodeRef)) + { + try + { + // Delete the file + + nodeService.deleteNode(nodeRef); + + // Remove the file state + + if ( ctx.hasStateTable()) + ctx.getStateTable().removeFileState(file.getFullName()); + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Delete on close - access denied, " + file.getFullName()); + + // Convert to a filesystem access denied exception + + throw new AccessDeniedException("Delete on close " + file.getFullName()); + } + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Closed file: \n" + + " network file: " + file + "\n" + + " deleted on close: " + file.hasDeleteOnClose()); + } + } + + /** + * Delete the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param file NetworkFile + * @exception java.io.IOException The exception description. + */ + public void deleteFile(SrvSession sess, TreeConnection tree, String name) throws IOException + { + // Create the transaction + + sess.beginTransaction(transactionService, false); + + // Get the device context + + ContentContext ctx = (ContentContext) tree.getContext(); + + try + { + // get the node + NodeRef nodeRef = getNodeForPath(tree, name); + if (nodeService.exists(nodeRef)) + { + nodeService.deleteNode(nodeRef); + + // Remove the file state + + if ( ctx.hasStateTable()) + ctx.getStateTable().removeFileState(name); + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Deleted file: \n" + + " file: " + name + "\n" + + " node: " + nodeRef); + } + } + catch (FileNotFoundException e) + { + // already gone + if (logger.isDebugEnabled()) + { + logger.debug("Deleted file : \n" + + " file: " + name); + } + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Delete file - access denied"); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Delete " + name); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Delete file error", ex); + + // Convert to a general I/O exception + + throw new IOException("Delete file " + name); + } + } + + /** + * Rename the specified file. + * + * @param sess Server session + * @param tree Tree connection + * @param oldName java.lang.String + * @param newName java.lang.String + * @exception java.io.IOException The exception description. + */ + public void renameFile(SrvSession sess, TreeConnection tree, String oldName, String newName) throws IOException + { + // Create the transaction + sess.beginTransaction(transactionService, false); + + try + { + // Get the device context + + ContentContext ctx = (ContentContext) tree.getContext(); + + // Get the file/folder to move + NodeRef nodeToMoveRef = getNodeForPath(tree, oldName); + + // Get the new target folder - it must be a folder + String[] splitPaths = FileName.splitPath(newName); + NodeRef targetFolderRef = getNodeForPath(tree, splitPaths[0]); + String name = splitPaths[1]; // the new file or folder name + + // Update the state table + boolean relinked = false; + if ( ctx.hasStateTable()) + { + // Check if the file rename can be relinked to a previous version + + if ( !cifsHelper.isDirectory(nodeToMoveRef) ) + { + // Check if there is a renamed file state for the new file name + + FileState renState = ctx.getStateTable().removeFileState(newName); + + if ( renState != null && renState.getFileStatus() == FileStateStatus.Renamed) + { + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug(" Found rename state, relinking, " + renState); + + // Relink the new version of the file data to the previously renamed node so that it + // picks up version history and other settings. + + cifsHelper.relinkNode( renState.getNodeRef(), nodeToMoveRef, targetFolderRef, name); + relinked = true; + + // Link the node ref for the associated rename state + + if ( renState.hasRenameState()) + renState.getRenameState().setNodeRef(nodeToMoveRef); + + // Remove the file state for the old file name + + ctx.getStateTable().removeFileState(oldName); + + // Get, or create, a file state for the new file path + + FileState fstate = ctx.getStateTable().findFileState(newName, false, true); + + fstate.setNodeRef(renState.getNodeRef()); + fstate.setFileStatus(FileStateStatus.FileExists); + } + else + { + // Get or create a new file state for the old file path + + FileState fstate = ctx.getStateTable().findFileState(oldName, false, true); + + // Make sure the file state is cached for a short while, the file may not be open so the + // file state could be expired + + fstate.setExpiryTime(System.currentTimeMillis() + FileState.RenameTimeout); + + // Indicate that this is a renamed file state, set the node ref of the file that was renamed + + fstate.setFileStatus(FileStateStatus.Renamed); + fstate.setNodeRef(nodeToMoveRef); + + // Get, or create, a file state for the new file path + + FileState newState = ctx.getStateTable().findFileState(newName, false, true); + + newState.setNodeRef(nodeToMoveRef); + newState.setFileStatus(FileStateStatus.FileExists); + + // Link the renamed state to the new state + + fstate.setRenameState(newState); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Cached rename state for " + oldName + ", state=" + fstate); + } + } + else + { + // Get the file state for the folder, if available + + FileState fstate = ctx.getStateTable().findFileState(oldName); + + if ( fstate != null) + { + // Update the file state index to use the new name + + ctx.getStateTable().renameFileState(newName, fstate); + } + } + } + + if (!relinked) + { + cifsHelper.move(nodeToMoveRef, targetFolderRef, name); + } + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug("Moved node: " + " from: " + oldName + " to: " + newName); + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Rename file - access denied, " + oldName); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Rename file " + oldName); + } + catch (NodeLockedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Rename file", ex); + + // Convert to an filesystem access denied exception + + throw new AccessDeniedException("Node locked " + oldName); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Rename file", ex); + + // Convert to a general I/O exception + + throw new IOException("Rename file " + oldName); + } + } + + /** + * Set file information + * + * @param sess SrvSession + * @param tree TreeConnection + * @param name String + * @param info FileInfo + * @exception IOException + */ + public void setFileInformation(SrvSession sess, TreeConnection tree, String name, FileInfo info) throws IOException + { + try + { + // Get the file/folder node + + NodeRef nodeRef = getNodeForPath(tree, name); + + // Check permissions on the file/folder node + + if ( permissionService.hasPermission(nodeRef, PermissionService.WRITE) == AccessStatus.DENIED) + throw new AccessDeniedException("No write access to " + name); + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Set file information - access denied, " + name); + + // Convert to a filesystem access denied status + + throw new AccessDeniedException("Set file information " + name); + } + catch (AlfrescoRuntimeException ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Open file error", ex); + + // Convert to a general I/O exception + + throw new IOException("Set file information " + name); + } + } + + /** + * Truncate a file to the specified size + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file details + * @param siz New file length + * @exception java.io.IOException The exception description. + */ + public void truncateFile(SrvSession sess, TreeConnection tree, NetworkFile file, long size) throws IOException + { + // Truncate or extend the file to the required size + + file.truncateFile(size); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Truncated file: \n" + + " network file: " + file + "\n" + + " size: " + size); + } + } + + /** + * Read a block of data from the specified file. + * + * @param sess Session details + * @param tree Tree connection + * @param file Network file + * @param buf Buffer to return data to + * @param bufPos Starting position in the return buffer + * @param siz Maximum size of data to return + * @param filePos File offset to read data + * @return Number of bytes read + * @exception java.io.IOException The exception description. + */ + public int readFile( + SrvSession sess, TreeConnection tree, NetworkFile file, + byte[] buffer, int bufferPosition, int size, long fileOffset) throws IOException + { + // Check if the file is a directory + + if(file.isDirectory()) + throw new AccessDeniedException(); + + // Read a block of data from the file + + int count = file.readFile(buffer, size, bufferPosition, fileOffset); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Read bytes from file: \n" + + " network file: " + file + "\n" + + " buffer size: " + buffer.length + "\n" + + " buffer pos: " + bufferPosition + "\n" + + " size: " + size + "\n" + + " file offset: " + fileOffset + "\n" + + " bytes read: " + count); + } + return count; + } + + /** + * Seek to the specified file position. + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file. + * @param pos Position to seek to. + * @param typ Seek type. + * @return New file position, relative to the start of file. + */ + public long seekFile(SrvSession sess, TreeConnection tree, NetworkFile file, long pos, int typ) throws IOException + { + throw new UnsupportedOperationException("Unsupported: " + file + " (seek)"); + } + + /** + * Write a block of data to the file. + * + * @param sess Server session + * @param tree Tree connection + * @param file Network file details + * @param buf byte[] Data to be written + * @param bufoff Offset within the buffer that the data starts + * @param siz int Data length + * @param fileoff Position within the file that the data is to be written. + * @return Number of bytes actually written + * @exception java.io.IOException The exception description. + */ + public int writeFile(SrvSession sess, TreeConnection tree, NetworkFile file, + byte[] buffer, int bufferOffset, int size, long fileOffset) throws IOException + { + // Write to the file + + file.writeFile(buffer, size, bufferOffset, fileOffset); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Wrote bytes to file: \n" + + " network file: " + file + "\n" + + " buffer size: " + buffer.length + "\n" + + " size: " + size + "\n" + + " file offset: " + fileOffset); + } + return size; + } + + /** + * Get the node for the specified path + * + * @param tree TreeConnection + * @param path String + * @return NodeRef + * @exception FileNotFoundException + */ + private NodeRef getNodeForPath(TreeConnection tree, String path) + throws FileNotFoundException + { + // Check if there is a cached state for the path + + ContentContext ctx = (ContentContext) tree.getContext(); + + if ( ctx.hasStateTable()) + { + // Try and get the node ref from an in memory file state + + FileState fstate = ctx.getStateTable().findFileState(path); + if ( fstate != null && fstate.hasNodeRef() && fstate.exists() ) + { + // check that the node exists + if (nodeService.exists(fstate.getNodeRef())) + { + return fstate.getNodeRef(); + } + else + { + ctx.getStateTable().removeFileState(path); + } + } + } + + // Search the repository for the node + + return cifsHelper.getNodeRef(ctx.getRootNode(), path); + } + + /** + * Connection opened to this disk device + * + * @param sess Server session + * @param tree Tree connection + */ + public void treeClosed(SrvSession sess, TreeConnection tree) + { + // Nothing to do + } + + /** + * Connection closed to this device + * + * @param sess Server session + * @param tree Tree connection + */ + public void treeOpened(SrvSession sess, TreeConnection tree) + { + // Nothing to do + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskInterface.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskInterface.java new file mode 100644 index 0000000000..2b69544847 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskInterface.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import org.alfresco.filesys.server.filesys.DiskInterface; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Extended {@link org.alfresco.filesys.server.filesys.DiskInterface disk interface} to + * allow access to some of the internal configuration properties. + * + * @author Derek Hulley + */ +public interface ContentDiskInterface extends DiskInterface +{ + /** + * Get the name of the shared path within the server. The share name is + * equivalent in browse path to the {@link #getContextRootNodeRef() context root}. + * + * @return Returns the share name + */ + public String getShareName(); + + /** + * Get a reference to the node that all CIFS paths are relative to + * + * @return Returns a node acting as the CIFS root + */ + public NodeRef getContextRootNodeRef(); +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java new file mode 100644 index 0000000000..4a9f089934 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.server.filesys.AccessDeniedException; +import org.alfresco.filesys.server.filesys.FileAttribute; +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.NetworkFile; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.RandomAccessContent; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.service.cmr.repository.ContentAccessor; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation of the NetworkFile for direct interaction + * with the channel repository. + *

    + * This provides the interaction with the Alfresco Content Model file/folder structure. + * + * @author Derek Hulley + */ +public class ContentNetworkFile extends NetworkFile +{ + private static final Log logger = LogFactory.getLog(ContentNetworkFile.class); + + private NodeService nodeService; + private ContentService contentService; + private NodeRef nodeRef; + /** keeps track of the read/write access */ + private FileChannel channel; + /** the original content opened */ + private ContentAccessor content; + /** keeps track of any writes */ + private boolean modified; + + // Flag to indicate if the file channel is writable + + private boolean writableChannel; + + /** + * Helper method to create a {@link NetworkFile network file} given a node reference. + * + * @param serviceRegistry + * @param filePathCache used to speed up repeated searches + * @param nodeRef the node representing the file or directory + * @param params the parameters dictating the path and other attributes with which the file is being accessed + * @return Returns a new instance of the network file + */ + public static ContentNetworkFile createFile( + NodeService nodeService, + ContentService contentService, + CifsHelper cifsHelper, + NodeRef nodeRef, + FileOpenParams params) + { + String path = params.getPath(); + + // Check write access + // TODO: Check access writes and compare to write requirements + + // create the file + ContentNetworkFile netFile = new ContentNetworkFile(nodeService, contentService, nodeRef, path); + // set relevant parameters + if (params.isReadOnlyAccess()) + { + netFile.setGrantedAccess(NetworkFile.READONLY); + } + else + { + netFile.setGrantedAccess(NetworkFile.READWRITE); + } + + // check the type + FileInfo fileInfo; + try + { + fileInfo = cifsHelper.getFileInformation(nodeRef, ""); + } + catch (FileNotFoundException e) + { + throw new AlfrescoRuntimeException("File not found when creating network file: " + nodeRef, e); + } + if (fileInfo.isDirectory()) + { + netFile.setAttributes(FileAttribute.Directory); + } + else + { + // Set the current size + + netFile.setFileSize(fileInfo.getSize()); + } + + // Set the file timestamps + + if ( fileInfo.hasCreationDateTime()) + netFile.setCreationDate( fileInfo.getCreationDateTime()); + + if ( fileInfo.hasModifyDateTime()) + netFile.setModifyDate(fileInfo.getModifyDateTime()); + + if ( fileInfo.hasAccessDateTime()) + netFile.setAccessDate(fileInfo.getAccessDateTime()); + + // Set the file attributes + + netFile.setAttributes(fileInfo.getFileAttributes()); + + // If the file is read-only then only allow read access + + if ( netFile.isReadOnly()) + netFile.setGrantedAccess(NetworkFile.READONLY); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created network file: \n" + + " node: " + nodeRef + "\n" + + " param: " + params + "\n" + + " netfile: " + netFile); + } + return netFile; + } + + private ContentNetworkFile(NodeService nodeService, ContentService contentService, NodeRef nodeRef, String name) + { + super(name); + setFullName(name); + this.nodeService = nodeService; + this.contentService = contentService; + this.nodeRef = nodeRef; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(50); + sb.append("ContentNetworkFile:") + .append("[ node=").append(nodeRef) + .append(", channel=").append(channel) + .append(writableChannel ? "(Write)" : "(Read)") + .append(", writable=").append(isWritable()) + .append(", content=").append(content) + .append(", modified=").append(modified) + .append("]"); + return sb.toString(); + } + + /** + * @return Returns the node reference representing this file + */ + public NodeRef getNodeRef() + { + return nodeRef; + } + + /** + * @return Returns true if the channel should be writable + * + * @see NetworkFile#getGrantedAccess() + * @see NetworkFile#READONLY + * @see NetworkFile#WRITEONLY + * @see NetworkFile#READWRITE + */ + private boolean isWritable() + { + // check that we are allowed to write + int access = getGrantedAccess(); + return (access == NetworkFile.READWRITE || access == NetworkFile.WRITEONLY); + } + + /** + * Opens the channel for reading or writing depending on the access mode. + *

    + * If the channel is already open, it is left. + * + * @param write true if the channel must be writable + * @throws AccessDeniedException if this network file is read only + * @throws AlfrescoRuntimeException if this network file represents a directory + * + * @see NetworkFile#getGrantedAccess() + * @see NetworkFile#READONLY + * @see NetworkFile#WRITEONLY + * @see NetworkFile#READWRITE + */ + private synchronized void openContent(boolean write) throws AccessDeniedException, AlfrescoRuntimeException + { + if (isDirectory()) + { + throw new AlfrescoRuntimeException("Unable to open channel for a directory network file: " + this); + } + + // Check if write access is required and the current channel is read-only + + else if ( write && writableChannel == false && channel != null) + { + // Close the existing read-only channel + + try + { + channel.close(); + channel = null; + } + catch (IOException ex) + { + logger.error("Error closing read-only channel", ex); + } + + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Switching to writable channel for " + getName()); + } + else if (channel != null) + { + // already have channel open + return; + } + + // we need to create the channel + if (write && !isWritable()) + { + throw new AccessDeniedException("The network file was created for read-only: " + this); + } + + content = null; + if (write) + { + content = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, false); + + // Indicate that we have a writable channel to the file + + writableChannel = true; + } + else + { + content = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + // ensure that the content we are going to read is valid + content = FileContentReader.getSafeContentReader( + (ContentReader) content, + I18NUtil.getMessage("content.content_missing"), + nodeRef, content); + + // Indicate that we only have a read-only channel to the data + + writableChannel = false; + } + // wrap the channel accessor, if required + if (!(content instanceof RandomAccessContent)) + { + // TODO: create a temp, random access file and put a FileContentWriter on it + // barf for now + throw new AlfrescoRuntimeException("Can only use a store that supplies randomly accessible channel"); + } + RandomAccessContent randAccessContent = (RandomAccessContent) content; + // get the channel - we can only make this call once + channel = randAccessContent.getChannel(); + } + + @Override + public synchronized void closeFile() throws IOException + { + if (isDirectory()) // ignore if this is a directory + { + return; + } + else if (channel == null) // ignore if the channel hasn't been opened + { + return; + } + else if (modified) // file was modified + { + // close it + channel.close(); + channel = null; + // write properties + ContentData contentData = content.getContentData(); + nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData); + } + else + { + // close it - it was not modified + channel.close(); + channel = null; + } + } + + @Override + public synchronized void truncateFile(long size) throws IOException + { + // open the channel for writing + openContent(true); + // truncate the channel + channel.truncate(size); + // set modification flag + modified = true; + // done + if (logger.isDebugEnabled()) + { + logger.debug("Truncated channel: " + + " net file: " + this + "\n" + + " size: " + size); + } + } + + /** + * Write a block of data to the file. + * + * @param buf byte[] + * @param len int + * @param pos int + * @param fileOff long + * @exception IOException + */ + public synchronized void writeFile(byte[] buffer, int length, int position, long fileOffset) throws IOException + { + // Open the channel for writing + + openContent(true); + + // Write to the channel + + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, position, length); + int count = channel.write(byteBuffer, fileOffset); + + // Set modification flag + + modified = true; + + // Update the current file size + + setFileSize(channel.size()); + + // DEBUG + + if (logger.isDebugEnabled()) + { + logger.debug("Wrote to channel: " + + " net file: " + this + "\n" + + " written: " + count); + } + } + + /** + * Read from the file. + * + * @param buf byte[] + * @param len int + * @param pos int + * @param fileOff long + * @return Length of data read. + * @exception IOException + */ + public synchronized int readFile(byte[] buffer, int length, int position, long fileOffset) throws IOException + { + // open the channel for reading + openContent(false); + + // read from the channel + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, position, length); + int count = channel.read(byteBuffer, fileOffset); + if (count < 0) + { + count = 0; // doesn't obey the same rules, i.e. just returns the bytes read + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Read from channel: " + + " net file: " + this + "\n" + + " read: " + count); + } + return count; + } + + @Override + public synchronized void openFile(boolean createFlag) throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized long seekFile(long pos, int typ) throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized void flushFile() throws IOException + { + // open the channel for writing + openContent(true); + // flush the channel - metadata flushing is not important + channel.force(false); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Flushed channel: " + + " net file: " + this); + } + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentSearchContext.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentSearchContext.java new file mode 100644 index 0000000000..b029316161 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentSearchContext.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import java.io.FileNotFoundException; +import java.util.List; + +import org.alfresco.filesys.server.filesys.FileInfo; +import org.alfresco.filesys.server.filesys.SearchContext; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Wrapper for simple XPath searche against the node service. The search is performed statically + * outside the context instance itself - this class merely maintains the state of the search + * results across client connections. + * + * @author Derek Hulley + */ +public class ContentSearchContext extends SearchContext +{ + private static final Log logger = LogFactory.getLog(ContentSearchContext.class); + + private CifsHelper cifsHelper; + private List results; + private int index = -1; + + /** + * Performs a search against the direct children of the given node. + *

    + * Wildcard characters are acceptable, and the search may either be for + * a specific file or directory, or any file or directory. + * + * @param serviceRegistry used to gain access the the repository + * @param cifsHelper caches path query results + * @param searchRootNodeRef the node whos children are to be searched + * @param searchStr the search string relative to the search root node + * @param attributes the search attributes, e.g. searching for folders, etc + * @return Returns a search context with the results of the search + */ + public static ContentSearchContext search( + CifsHelper cifsHelper, + NodeRef searchRootNodeRef, + String searchStr, + int attributes) + { + // perform the search + List results = cifsHelper.getNodeRefs(searchRootNodeRef, searchStr); + + // build the search context to store the results + ContentSearchContext searchCtx = new ContentSearchContext(cifsHelper, results, searchStr); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Search context created: \n" + + " search root: " + searchRootNodeRef + "\n" + + " search context: " + searchCtx); + } + return searchCtx; + } + + /** + * @see ContentSearchContext#search(FilePathCache, NodeRef, String, int) + */ + private ContentSearchContext( + CifsHelper cifsHelper, + List results, + String searchStr) + { + super(); + super.setSearchString(searchStr); + this.cifsHelper = cifsHelper; + this.results = results; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(60); + sb.append("ContentSearchContext") + .append("[ searchStr=").append(getSearchString()) + .append(", resultCount=").append(results.size()) + .append("]"); + return sb.toString(); + } + + @Override + public synchronized int getResumeId() + { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized boolean hasMoreFiles() + { + return index < (results.size() -1); + } + + @Override + public synchronized boolean nextFileInfo(FileInfo info) + { + // check if there is anything else to return + if (!hasMoreFiles()) + { + return false; + } + // increment the index + index++; + // get the next file info + NodeRef nextNodeRef = results.get(index); + // get the file info + + try + { + FileInfo nextInfo = cifsHelper.getFileInformation(nextNodeRef, ""); + // copy to info handle + info.copyFrom(nextInfo); + + // success + return true; + } + catch (FileNotFoundException e) + { + return false; + } + } + + @Override + public synchronized String nextFileName() + { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized boolean restartAt(FileInfo info) + { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized boolean restartAt(int resumeId) + { + throw new UnsupportedOperationException(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/FileState.java b/source/java/org/alfresco/filesys/smb/server/repo/FileState.java new file mode 100644 index 0000000000..74f9b851b8 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/FileState.java @@ -0,0 +1,619 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import org.alfresco.filesys.locking.FileLock; +import org.alfresco.filesys.locking.FileLockList; +import org.alfresco.filesys.locking.LockConflictException; +import org.alfresco.filesys.locking.NotLockedException; +import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.filesys.server.filesys.FileOpenParams; +import org.alfresco.filesys.server.filesys.FileStatus; +import org.alfresco.filesys.smb.SharingMode; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * File State Class + * + *

    Keeps track of file state across all sessions on the server, to keep track of file sharing modes, + * file locks and also for synchronizing access to files/folders. + * + * @author gkspencer + */ +public class FileState +{ + private static final Log logger = LogFactory.getLog(FileState.class); + + // File state constants + + public final static long NoTimeout = -1L; + public final static long DefTimeout = 5 * 60000L; // 5 minutes + public final static long RenameTimeout = 1 * 60000L; // 1 minute + + // File status + + public enum FileStateStatus { NotExist, FileExists, FolderExists, Renamed }; + + // File name/path + + private String m_path; + + // File state timeout, -1 indicates no timeout + + private long m_tmo; + + // File status, indicates if the file/folder exists and if it is a file or folder. + + private FileStateStatus m_fileStatus = FileStateStatus.NotExist; + + // Open file count + + private int m_openCount; + + // Sharing mode + + private int m_sharedAccess = SharingMode.READWRITE; + + // File lock list, allocated once there are active locks on this file + + private FileLockList m_lockList; + + // Node for this file + + private NodeRef m_nodeRef; + + // Link to the new file state when a file is renamed + + private FileState m_newNameState; + + /** + * Class constructor + * + * @param fname String + * @param isdir boolean + */ + public FileState(String fname, boolean isdir) + { + + // Normalize the file path + + setPath(fname); + setExpiryTime(System.currentTimeMillis() + DefTimeout); + + // Set the file/folder status + + setFileStatus( isdir ? FileStateStatus.FolderExists : FileStateStatus.FileExists); + } + + /** + * Return the file name/path + * + * @return String + */ + public final String getPath() + { + return m_path; + } + + /** + * Return the file status + * + * @return FileStateStatus + */ + public final FileStateStatus getFileStatus() + { + return m_fileStatus; + } + + /** + * Determine if the file/folder exists + * + * @return boolen + */ + public final boolean exists() + { + if ( m_fileStatus == FileStateStatus.FileExists || + m_fileStatus == FileStateStatus.FolderExists) + return true; + return false; + } + + /** + * Return the directory state + * + * @return boolean + */ + public final boolean isDirectory() + { + return m_fileStatus == FileStateStatus.FolderExists ? true : false; + } + + /** + * Determine if the associated node has been set + * + * @return boolean + */ + public final boolean hasNodeRef() + { + return m_nodeRef != null ? true : false; + } + + /** + * Return the associated node + * + * @return NodeRef + */ + public final NodeRef getNodeRef() + { + return m_nodeRef; + } + + /** + * Return the file open count + * + * @return int + */ + public final int getOpenCount() + { + return m_openCount; + } + + /** + * Return the shared access mode + * + * @return int + */ + public final int getSharedAccess() + { + return m_sharedAccess; + } + + /** + * Check if there are active locks on this file + * + * @return boolean + */ + public final boolean hasActiveLocks() + { + if (m_lockList != null && m_lockList.numberOfLocks() > 0) + return true; + return false; + } + + /** + * Check if this file state does not expire + * + * @return boolean + */ + public final boolean hasNoTimeout() + { + return m_tmo == NoTimeout ? true : false; + } + + /** + * Check if the file can be opened depending on any current file opens and the sharing mode of + * the first file open + * + * @param params FileOpenParams + * @return boolean + */ + public final boolean allowsOpen(FileOpenParams params) + { + + // If the file is not currently open then allow the file open + + if (getOpenCount() == 0) + return true; + + // Check the shared access mode + + if (getSharedAccess() == SharingMode.READWRITE && params.getSharedAccess() == SharingMode.READWRITE) + return true; + else if ((getSharedAccess() & SharingMode.READ) != 0 && params.isReadOnlyAccess()) + return true; + else if ((getSharedAccess() & SharingMode.WRITE) != 0 && params.isWriteOnlyAccess()) + return true; + + // Sharing violation, do not allow the file open + + return false; + } + + /** + * Increment the file open count + * + * @return int + */ + public final synchronized int incrementOpenCount() + { + m_openCount++; + + // Debug + + // if ( logger.isDebugEnabled() && m_openCount > 1) + // logger.debug("@@@@@ File open name=" + getPath() + ", count=" + m_openCount); + return m_openCount; + } + + /** + * Decrement the file open count + * + * @return int + */ + public final synchronized int decrementOpenCount() + { + + // Debug + + if (m_openCount <= 0) + logger.debug("@@@@@ File close name=" + getPath() + ", count=" + m_openCount + " <>"); + else + m_openCount--; + + return m_openCount; + } + + /** + * Check if the file state has expired + * + * @param curTime long + * @return boolean + */ + public final boolean hasExpired(long curTime) + { + if (m_tmo == NoTimeout) + return false; + if (curTime > m_tmo) + return true; + return false; + } + + /** + * Return the number of seconds left before the file state expires + * + * @param curTime long + * @return long + */ + public final long getSecondsToExpire(long curTime) + { + if (m_tmo == NoTimeout) + return -1; + return (m_tmo - curTime) / 1000L; + } + + /** + * Determine if the file state has an associated rename state + * + * @return boolean + */ + public final boolean hasRenameState() + { + return m_newNameState != null ? true : false; + } + + /** + * Return the associated rename state + * + * @return FileState + */ + public final FileState getRenameState() + { + return m_newNameState; + } + + /** + * Set the file status + * + * @param status FileStateStatus + */ + public final void setFileStatus(FileStateStatus status) + { + m_fileStatus = status; + } + + /** + * Set the file status + * + * @param fsts int + */ + public final void setFileStatus(int fsts) + { + if ( fsts == FileStatus.FileExists) + m_fileStatus = FileStateStatus.FileExists; + else if ( fsts == FileStatus.DirectoryExists) + m_fileStatus = FileStateStatus.FolderExists; + else if ( fsts == FileStatus.NotExist) + m_fileStatus = FileStateStatus.NotExist; + } + + /** + * Set the file state expiry time + * + * @param expire long + */ + public final void setExpiryTime(long expire) + { + m_tmo = expire; + } + + /** + * Set the node ref for the file/folder + * + * @param nodeRef NodeRef + */ + public final void setNodeRef(NodeRef nodeRef) + { + m_nodeRef = nodeRef; + } + + /** + * Set the associated file state when a file is renamed, this is the link to the new file state + * + * @param fstate FileState + */ + public final void setRenameState(FileState fstate) + { + m_newNameState = fstate; + } + + /** + * Set the shared access mode, from the first file open + * + * @param mode int + */ + public final void setSharedAccess(int mode) + { + if (getOpenCount() == 0) + m_sharedAccess = mode; + } + + /** + * Set the file path + * + * @param path String + */ + public final void setPath(String path) + { + + // Split the path into directories and file name, only uppercase the directories to + // normalize the path. + + m_path = normalizePath(path); + } + + /** + * Return the count of active locks on this file + * + * @return int + */ + public final int numberOfLocks() + { + if (m_lockList != null) + return m_lockList.numberOfLocks(); + return 0; + } + + /** + * Add a lock to this file + * + * @param lock FileLock + * @exception LockConflictException + */ + public final void addLock(FileLock lock) throws LockConflictException + { + + // Check if the lock list has been allocated + + if (m_lockList == null) + { + + synchronized (this) + { + + // Allocate the lock list, check if the lock list has been allocated elsewhere + // as we may have been waiting for the lock + + if (m_lockList == null) + m_lockList = new FileLockList(); + } + } + + // Add the lock to the list, check if there are any lock conflicts + + synchronized (m_lockList) + { + + // Check if the new lock overlaps with any existing locks + + if (m_lockList.allowsLock(lock)) + { + + // Add the new lock to the list + + m_lockList.addLock(lock); + } + else + throw new LockConflictException(); + } + } + + /** + * Remove a lock on this file + * + * @param lock FileLock + * @exception NotLockedException + */ + public final void removeLock(FileLock lock) throws NotLockedException + { + + // Check if the lock list has been allocated + + if (m_lockList == null) + throw new NotLockedException(); + + // Remove the lock from the active list + + synchronized (m_lockList) + { + + // Remove the lock, check if we found the matching lock + + if (m_lockList.removeLock(lock) == null) + throw new NotLockedException(); + } + } + + /** + * Check if the file is readable for the specified section of the file and process id + * + * @param offset long + * @param len long + * @param pid int + * @return boolean + */ + public final boolean canReadFile(long offset, long len, int pid) + { + + // Check if the lock list is valid + + if (m_lockList == null) + return true; + + // Check if the file section is readable by the specified process + + boolean readOK = false; + + synchronized (m_lockList) + { + + // Check if the file section is readable + + readOK = m_lockList.canReadFile(offset, len, pid); + } + + // Return the read status + + return readOK; + } + + /** + * Check if the file is writeable for the specified section of the file and process id + * + * @param offset long + * @param len long + * @param pid int + * @return boolean + */ + public final boolean canWriteFile(long offset, long len, int pid) + { + + // Check if the lock list is valid + + if (m_lockList == null) + return true; + + // Check if the file section is writeable by the specified process + + boolean writeOK = false; + + synchronized (m_lockList) + { + + // Check if the file section is writeable + + writeOK = m_lockList.canWriteFile(offset, len, pid); + } + + // Return the write status + + return writeOK; + } + + /** + * Normalize the path to uppercase the directory names and keep the case of the file name. + * + * @param path String + * @return String + */ + public final static String normalizePath(String path) + { + + // Split the path into directories and file name, only uppercase the directories to + // normalize the path. + + String normPath = path; + + if (path.length() > 3) + { + + // Split the path to seperate the folders/file name + + int pos = path.lastIndexOf(FileName.DOS_SEPERATOR); + if (pos != -1) + { + + // Get the path and file name parts, normalize the path + + String pathPart = path.substring(0, pos).toUpperCase(); + String namePart = path.substring(pos); + + // Rebuild the path string + + normPath = pathPart + namePart; + } + } + + // Return the normalized path + + return normPath; + } + + /** + * Return the file state as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("["); + str.append(getPath()); + str.append(","); + str.append(getFileStatus()); + str.append(":Opn="); + str.append(getOpenCount()); + + str.append(",Expire="); + str.append(getSecondsToExpire(System.currentTimeMillis())); + + str.append(",Locks="); + str.append(numberOfLocks()); + + str.append(",Ref="); + if ( hasNodeRef()) + str.append(getNodeRef().getId()); + else + str.append("Null"); + + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/repo/FileStateTable.java b/source/java/org/alfresco/filesys/smb/server/repo/FileStateTable.java new file mode 100644 index 0000000000..d2563f3669 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/repo/FileStateTable.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.repo; + +import java.util.*; +import java.io.*; + +import org.apache.commons.logging.*; + +/** + * File State Table Class + *

    + * Contains an indexed list of the currently open files/folders. + */ +public class FileStateTable implements Runnable +{ + private static final Log logger = LogFactory.getLog(FileStateTable.class); + + // Initial allocation size for the state cache + + private static final int INITIAL_SIZE = 100; + + // Default expire check thread interval + + private static final long DEFAULT_EXPIRECHECK = 15000; + + // File state table, keyed by file path + + private Hashtable m_stateTable; + + // Wakeup interval for the expire file state checker thread + + private long m_expireInterval = DEFAULT_EXPIRECHECK; + + // File state expiry time in seconds + + private long m_cacheTimer = 2 * 60000L; // 2 minutes default + + /** + * Class constructor + */ + public FileStateTable() + { + m_stateTable = new Hashtable(INITIAL_SIZE); + + // Start the expired file state checker thread + + Thread th = new Thread(this); + th.setDaemon(true); + th.setName("FileStateExpire"); + th.start(); + } + + /** + * Return the expired file state checker interval, in milliseconds + * + * @return long + */ + public final long getCheckInterval() + { + return m_expireInterval; + } + + /** + * Get the file state cache timer, in milliseconds + * + * @return long + */ + public final long getCacheTimer() + { + return m_cacheTimer; + } + + /** + * Return the number of states in the cache + * + * @return int + */ + public final int numberOfStates() + { + return m_stateTable.size(); + } + + /** + * Set the default file state cache timer, in milliseconds + * + * @param tmo long + */ + public final void setCacheTimer(long tmo) + { + m_cacheTimer = tmo; + } + + /** + * Set the expired file state checker interval, in milliseconds + * + * @param chkIntval long + */ + public final void setCheckInterval(long chkIntval) + { + m_expireInterval = chkIntval; + } + + /** + * Add a new file state + * + * @param fstate FileState + */ + public final synchronized void addFileState(FileState fstate) + { + + // Check if the file state already exists in the cache + + if (logger.isDebugEnabled() && m_stateTable.get(fstate.getPath()) != null) + logger.debug("***** addFileState() state=" + fstate.toString() + " - ALREADY IN CACHE *****"); + + // DEBUG + + if (logger.isDebugEnabled() && fstate == null) + { + logger.debug("addFileState() NULL FileState"); + return; + } + + // Set the file state timeout and add to the cache + + fstate.setExpiryTime(System.currentTimeMillis() + getCacheTimer()); + m_stateTable.put(fstate.getPath(), fstate); + } + + /** + * Find the file state for the specified path + * + * @param path String + * @return FileState + */ + public final synchronized FileState findFileState(String path) + { + return m_stateTable.get(FileState.normalizePath(path)); + } + + /** + * Find the file state for the specified path, and optionally create a new file state if not + * found + * + * @param path String + * @param isdir boolean + * @param create boolean + * @return FileState + */ + public final synchronized FileState findFileState(String path, boolean isdir, boolean create) + { + + // Find the required file state, if it exists + + FileState state = m_stateTable.get(FileState.normalizePath(path)); + + // Check if we should create a new file state + + if (state == null && create == true) + { + + // Create a new file state + + state = new FileState(path, isdir); + + // Set the file state timeout and add to the cache + + state.setExpiryTime(System.currentTimeMillis() + getCacheTimer()); + m_stateTable.put(state.getPath(), state); + } + + // Return the file state + + return state; + } + + /** + * Update the name that a file state is cached under, and the associated file state + * + * @param oldName String + * @param newName String + * @return FileState + */ + public final synchronized FileState updateFileState(String oldName, String newName) + { + + // Find the current file state + + FileState state = m_stateTable.remove(FileState.normalizePath(oldName)); + + // Rename the file state and add it back into the cache using the new name + + if (state != null) + { + state.setPath(newName); + addFileState(state); + } + + // Return the updated file state + + return state; + } + + /** + * Enumerate the file state cache + * + * @return Enumeration + */ + public final Enumeration enumerate() + { + return m_stateTable.keys(); + } + + /** + * Remove the file state for the specified path + * + * @param path String + * @return FileState + */ + public final synchronized FileState removeFileState(String path) + { + + // Remove the file state from the cache + + FileState state = m_stateTable.remove(FileState.normalizePath(path)); + + // Return the removed file state + + return state; + } + + /** + * Rename a file state, remove the existing entry, update the path and add the state back into + * the cache using the new path. + * + * @param newPath String + * @param state FileState + */ + public final synchronized void renameFileState(String newPath, FileState state) + { + + // Remove the existing file state from the cache, using the original name + + m_stateTable.remove(state.getPath()); + + // Update the file state path and add it back to the cache using the new name + + state.setPath(FileState.normalizePath(newPath)); + m_stateTable.put(state.getPath(), state); + } + + /** + * Remove all file states from the cache + */ + public final synchronized void removeAllFileStates() + { + + // Check if there are any items in the cache + + if (m_stateTable == null || m_stateTable.size() == 0) + return; + + // Enumerate the file state cache and remove expired file state objects + + Enumeration enm = m_stateTable.keys(); + + while (enm.hasMoreElements()) + { + + // Get the file state + + FileState state = m_stateTable.get(enm.nextElement()); + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug("++ Closed: " + state.getPath()); + } + + // Remove all the file states + + m_stateTable.clear(); + } + + /** + * Remove expired file states from the cache + * + * @return int + */ + public final int removeExpiredFileStates() + { + + // Check if there are any items in the cache + + if (m_stateTable == null || m_stateTable.size() == 0) + return 0; + + // Enumerate the file state cache and remove expired file state objects + + Enumeration enm = m_stateTable.keys(); + long curTime = System.currentTimeMillis(); + + int expiredCnt = 0; + + while (enm.hasMoreElements()) + { + + // Get the file state + + FileState state = m_stateTable.get(enm.nextElement()); + + if (state != null && state.hasNoTimeout() == false) + { + + synchronized (state) + { + + // Check if the file state has expired and there are no open references to the + // file + + if (state.hasExpired(curTime) && state.getOpenCount() == 0) + { + + // Remove the expired file state + + m_stateTable.remove(state.getPath()); + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug("++ Expired file state: " + state); + + // Update the expired count + + expiredCnt++; + } + } + } + } + + // Return the count of expired file states that were removed + + return expiredCnt; + } + + /** + * Expired file state checker thread + */ + public void run() + { + + // Loop forever + + while (true) + { + + // Sleep for the required interval + + try + { + Thread.sleep(getCheckInterval()); + } + catch (InterruptedException ex) + { + } + + try + { + + // Check for expired file states + + int cnt = removeExpiredFileStates(); + + // Debug + + if (logger.isDebugEnabled() && cnt > 0) + { + logger.debug("++ Expired " + cnt + " file states, cache=" + m_stateTable.size()); + Dump(); + } + } + catch (Exception ex) + { + logger.debug(ex); + } + } + } + + /** + * Dump the state cache entries to the specified stream + */ + public final void Dump() + { + + // Dump the file state cache entries to the specified stream + + if (m_stateTable.size() > 0) + logger.info("++ FileStateCache Entries:"); + + Enumeration enm = m_stateTable.keys(); + long curTime = System.currentTimeMillis(); + + while (enm.hasMoreElements()) + { + String fname = (String) enm.nextElement(); + FileState state = m_stateTable.get(fname); + + logger.info(" ++ " + fname + "(" + state.getSecondsToExpire(curTime) + ") : " + state); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/win32/LanaListener.java b/source/java/org/alfresco/filesys/smb/server/win32/LanaListener.java new file mode 100644 index 0000000000..df2133d26c --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/win32/LanaListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +package org.alfresco.filesys.smb.server.win32; + +/** + * LANA Listener Class + * + *

    Receive status change events for a particular NetBIOS LANA. + * + * @author GKSpencer + */ +public interface LanaListener +{ + /** + * LANA status change callback + * + * @param lana int + * @param online boolean + */ + public void lanaStatusChange( int lana, boolean online); +} diff --git a/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSLanaMonitor.java b/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSLanaMonitor.java new file mode 100644 index 0000000000..8cd03c20ce --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSLanaMonitor.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.win32; + +import java.util.BitSet; + +import org.alfresco.filesys.netbios.win32.NetBIOSSocket; +import org.alfresco.filesys.netbios.win32.Win32NetBIOS; +import org.alfresco.filesys.netbios.win32.WinsockNetBIOSException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.smb.mailslot.Win32NetBIOSHostAnnouncer; +import org.alfresco.filesys.smb.server.SMBServer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Win32 NetBIOS LANA Monitor Class + *

    + * Monitors the available NetBIOS LANAs to check for new network interfaces coming online. A session + * socket handler will be created for new LANAs as they appear. + */ +public class Win32NetBIOSLanaMonitor extends Thread +{ + // Constants + // + // Initial LANA listener array size + + private static final int LanaListenerArraySize = 16; + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Global LANA monitor + + private static Win32NetBIOSLanaMonitor _lanaMonitor; + + // Available LANA list and current status + + private BitSet m_lanas; + private BitSet m_lanaSts; + + // LANA status listeners + + private LanaListener[] m_listeners; + + // SMB/CIFS server to add new session handlers to + + private SMBServer m_server; + + // Wakeup interval + + private long m_wakeup; + + // Shutdown request flag + + private boolean m_shutdown; + + // Debug output enable + + private boolean m_debug; + + /** + * Class constructor + * + * @param server SMBServer + * @param lanas int[] + * @param wakeup long + * @param debug boolean + */ + Win32NetBIOSLanaMonitor(SMBServer server, int[] lanas, long wakeup, boolean debug) + { + + // Set the SMB server and wakeup interval + + m_server = server; + m_wakeup = wakeup; + + m_debug = debug; + + // Set the current LANAs in the available LANAs list + + m_lanas = new BitSet(); + m_lanaSts = new BitSet(); + + if (lanas != null) + { + + // Set the currently available LANAs + + for (int i = 0; i < lanas.length; i++) + m_lanas.set(lanas[i]); + } + + // Initialize the online LANA status list + + int[] curLanas = Win32NetBIOS.LanaEnumerate(); + + if ( curLanas != null) + { + for ( int i = 0; i < curLanas.length; i++) + m_lanaSts.set(curLanas[i], true); + } + + // Set the global LANA monitor, if not already set + + if (_lanaMonitor == null) + _lanaMonitor = this; + + // Start the LANA monitor thread + + setDaemon(true); + start(); + } + + /** + * Return the global LANA monitor + * + * @return Win32NetBIOSLanaMonitor + */ + public static Win32NetBIOSLanaMonitor getLanaMonitor() + { + return _lanaMonitor; + } + + /** + * Add a LANA listener + * + * @param lana int + * @param listener LanaListener + */ + public synchronized final void addLanaListener(int lana, LanaListener l) + { + // Range check the LANA id + + if ( lana < 0 || lana > 255) + return; + + // Check if the listener array has been allocated + + if ( m_listeners == null) + { + int len = LanaListenerArraySize; + if ( lana > len) + len = (lana + 3) & 0x00FC; + + m_listeners = new LanaListener[len]; + } + else if ( lana > m_listeners.length) + { + // Extend the LANA listener array + + LanaListener[] newArray = new LanaListener[(lana + 3) & 0x00FC]; + + // Copy the existing array to the extended array + + System.arraycopy(m_listeners, 0, newArray, 0, m_listeners.length); + m_listeners = newArray; + } + + // Add the LANA listener + + m_listeners[lana] = l; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS register listener for LANA " + lana); + } + + /** + * Remove a LANA listener + * + * @param lana int + */ + public synchronized final void removeLanaListener(int lana) + { + // Validate the LANA id + + if ( m_listeners == null || lana < 0 || lana >= m_listeners.length) + return; + + m_listeners[lana] = null; + } + + /** + * Thread method + */ + public void run() + { + // Clear the shutdown flag + + m_shutdown = false; + + // If Winsock NetBIOS is not enabled then initialize the sockets interface + + ServerConfiguration config = m_server.getConfiguration(); + + if ( config.useWinsockNetBIOS() == false) + { + try + { + NetBIOSSocket.initializeSockets(); + } + catch (WinsockNetBIOSException ex) + { + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS initialization error", ex); + + // Shutdown the LANA monitor thread + + m_shutdown = true; + } + } + + // Loop until shutdown + + BitSet curLanas = new BitSet(); + + while (m_shutdown == false) + { + + // Wait for a network address change event + + Win32NetBIOS.waitForNetworkAddressChange(); + + // Check if the monitor has been closed + + if ( m_shutdown == true) + continue; + + // Clear the current active LANA bit set + + curLanas.clear(); + + // Get the available LANA list + + int[] lanas = Win32NetBIOS.LanaEnumerate(); + if (lanas != null) + { + + // Check if there are any new LANAs available + + Win32NetBIOSSessionSocketHandler sessHandler = null; + + for (int i = 0; i < lanas.length; i++) + { + + // Get the current LANA id, check if it's a known LANA + + int lana = lanas[i]; + curLanas.set(lana, true); + + if (m_lanas.get(lana) == false) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS found new LANA, " + lana); + + // Create a single Win32 NetBIOS session handler using the specified LANA + + sessHandler = new Win32NetBIOSSessionSocketHandler(m_server, lana, hasDebug()); + + try + { + sessHandler.initialize(); + } + catch (Exception ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS failed to create session handler for LANA " + lana, + ex); + + // Clear the session handler + + sessHandler = null; + } + + // If the session handler was initialized successfully add it to the + // SMB/CIFS server + + if (sessHandler != null) + { + + // Add the session handler to the SMB/CIFS server + + m_server.addSessionHandler(sessHandler); + + // Run the NetBIOS session handler in a seperate thread + + Thread nbThread = new Thread(sessHandler); + nbThread.setName("Win32NB_Handler_" + lana); + nbThread.start(); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS created session handler on LANA " + lana); + + // Check if a host announcer should be enabled + + if (config.hasWin32EnableAnnouncer()) + { + + // Create a host announcer + + Win32NetBIOSHostAnnouncer hostAnnouncer = new Win32NetBIOSHostAnnouncer(sessHandler, + config.getDomainName(), config.getWin32HostAnnounceInterval()); + + // Add the host announcer to the SMB/CIFS server list + + m_server.addHostAnnouncer(hostAnnouncer); + hostAnnouncer.start(); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS host announcer enabled on LANA " + lana); + } + + // Set the LANA in the available LANA list, and set the current status to online + + m_lanas.set(lana); + m_lanaSts.set(lana, true); + } + } + else + { + // Check if the LANA has just come back online + + if ( m_lanaSts.get(lana) == false) + { + // Change the LANA status to indicate the LANA is back online + + m_lanaSts.set(lana, true); + + // Inform the listener that the LANA is back online + + if ( m_listeners != null && lana < m_listeners.length && + m_listeners[lana] != null) + m_listeners[lana].lanaStatusChange(lana, true); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS LANA online - " + lana); + } + } + } + + // Check if there are any LANAs that have gone offline + + for ( int i = 0; i < m_lanaSts.length(); i++) + { + if ( curLanas.get(i) == false && m_lanaSts.get(i) == true) + { + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS LANA offline - " + i); + + // Change the LANA status + + m_lanaSts.set(i, false); + + // Check if there is an associated listener for the LANA + + if ( m_listeners != null && m_listeners[i] != null) + { + // Notify the LANA listener that the LANA is now offline + + m_listeners[i].lanaStatusChange(i, false); + } + } + } + } + } + } + + /** + * Determine if debug output is enabled + * + * @return boolean + */ + public final boolean hasDebug() + { + return m_debug; + } + + /** + * Request the LANA monitor thread to shutdown + */ + public final void shutdownRequest() + { + m_shutdown = true; + + // If Winsock NetBIOS is being used shutdown the Winsock interface + + if ( m_server.getConfiguration().useWinsockNetBIOS()) + NetBIOSSocket.shutdownSockets(); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSPacketHandler.java b/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSPacketHandler.java new file mode 100644 index 0000000000..0c9dcc7ad2 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSPacketHandler.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.win32; + +import java.io.IOException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.netbios.win32.NetBIOS; +import org.alfresco.filesys.netbios.win32.Win32NetBIOS; +import org.alfresco.filesys.smb.server.PacketHandler; +import org.alfresco.filesys.smb.server.SMBSrvPacket; + +/** + * Win32 NetBIOS Packet Handler Class + * + *

    Uses the Win32 Netbios() call to provide the low level session layer for better integration with + * Windows. + * + * @author GKSpencer + */ +public class Win32NetBIOSPacketHandler extends PacketHandler +{ + + // Constants + // + // Receive error encoding and length masks + + private static final int ReceiveErrorMask = 0xFF000000; + private static final int ReceiveLengthMask = 0x0000FFFF; + + // Network LAN adapter to use + + private int m_lana; + + // NetBIOS session id + + private int m_lsn; + + /** + * Class constructor + * + * @param lana int + * @param lsn int + * @param callerName String + */ + public Win32NetBIOSPacketHandler(int lana, int lsn, String callerName) + { + super(SMBSrvPacket.PROTOCOL_WIN32NETBIOS, "Win32NB", "WNB", callerName); + + m_lana = lana; + m_lsn = lsn; + } + + /** + * Return the LANA number + * + * @return int + */ + public final int getLANA() + { + return m_lana; + } + + /** + * Return the NetBIOS session id + * + * @return int + */ + public final int getLSN() + { + return m_lsn; + } + + /** + * Read a packet from the client + * + * @param pkt SMBSrvPacket + * @return int + * @throws IOException + */ + public int readPacket(SMBSrvPacket pkt) throws IOException + { + + // Wait for a packet on the Win32 NetBIOS session + // + // As Windows is handling the NetBIOS session layer we only receive the SMB packet. In order + // to be compatible with the other packet handlers we allow for the 4 byte header. + + int pktLen = pkt.getBuffer().length; + if (pktLen > NetBIOS.MaxReceiveSize) + pktLen = NetBIOS.MaxReceiveSize; + + int rxLen = Win32NetBIOS.Receive(m_lana, m_lsn, pkt.getBuffer(), 4, pktLen - 4); + + if ((rxLen & ReceiveErrorMask) != 0) + { + + // Check for an incomplete message status code + + int sts = (rxLen & ReceiveErrorMask) >> 24; + + if (sts == NetBIOS.NRC_Incomp) + { + + // Check if the packet buffer is already at the maximum size (we assume the maximum + // size is the maximum that RFC NetBIOS can send which is 17bits) + + if (pkt.getBuffer().length < RFCNetBIOSProtocol.MaxPacketSize) + { + + // Allocate a new buffer + + byte[] newbuf = new byte[RFCNetBIOSProtocol.MaxPacketSize]; + + // Copy the first part of the received data to the new buffer + + System.arraycopy(pkt.getBuffer(), 4, newbuf, 4, pktLen - 4); + + // Move the new buffer in as the main packet buffer + + pkt.setBuffer(newbuf); + + // DEBUG + + // Debug.println("readPacket() extended buffer to " + pkt.getBuffer().length); + } + + // Set the original receive size + + rxLen = (rxLen & ReceiveLengthMask); + + // Receive the remaining data + // + // Note: If the second read request is issued with a size of 64K or 64K-4 it returns + // with another incomplete status and returns no data. + + int rxLen2 = Win32NetBIOS.Receive(m_lana, m_lsn, pkt.getBuffer(), rxLen + 4, 32768); + + if ((rxLen2 & ReceiveErrorMask) != 0) + { + sts = (rxLen2 & ReceiveErrorMask) >> 24; + throw new IOException("Win32 NetBIOS multi-part receive failed, sts=0x" + sts + ", err=" + + NetBIOS.getErrorString(sts)); + } + + // Set the total received data length + + rxLen += rxLen2; + } + else + { + + // Indicate that the session has closed + + return -1; + } + } + + // Return the received data length + + return rxLen; + } + + /** + * Write a packet to the client + * + * @param pkt SMBSrvPacket + * @param len int + * @throws IOException + */ + public void writePacket(SMBSrvPacket pkt, int len) throws IOException + { + + // Output the packet on the Win32 NetBIOS session + // + // As Windows is handling the NetBIOS session layer we do not send the 4 byte header that is + // used by the NetBIOS over TCP/IP and native SMB packet handlers. + + int sts = Win32NetBIOS.Send(m_lana, m_lsn, pkt.getBuffer(), 4, len); + + // Do not check the status, if the session has been closed the next receive will fail + } + + /** + * Close the Win32 NetBIOS packet handler. Hangup the NetBIOS session + */ + public void closeHandler() + { + super.closeHandler(); + + // Hangup the Win32 NetBIOS session + + Win32NetBIOS.Hangup(m_lana, m_lsn); + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSSessionSocketHandler.java b/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSSessionSocketHandler.java new file mode 100644 index 0000000000..0a29399cc3 --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/win32/Win32NetBIOSSessionSocketHandler.java @@ -0,0 +1,1069 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.win32; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.filesys.netbios.NetBIOSName; +import org.alfresco.filesys.netbios.win32.NetBIOS; +import org.alfresco.filesys.netbios.win32.NetBIOSSocket; +import org.alfresco.filesys.netbios.win32.Win32NetBIOS; +import org.alfresco.filesys.netbios.win32.WinsockError; +import org.alfresco.filesys.netbios.win32.WinsockNetBIOSException; +import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.filesys.smb.mailslot.HostAnnouncer; +import org.alfresco.filesys.smb.mailslot.Win32NetBIOSHostAnnouncer; +import org.alfresco.filesys.smb.mailslot.WinsockNetBIOSHostAnnouncer; +import org.alfresco.filesys.smb.server.PacketHandler; +import org.alfresco.filesys.smb.server.SMBServer; +import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.alfresco.filesys.smb.server.SessionSocketHandler; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Win32 NetBIOS Session Socket Handler Class + * + *

    Uses the Win32 Netbios() call to provide the low level session layer for better integration with + * Windows. + * + * @author GKSpencer + */ +public class Win32NetBIOSSessionSocketHandler extends SessionSocketHandler implements LanaListener +{ + + // Debug logging + + private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol"); + + // Constants + // + // Default LANA offline polling interval + + public static final long LANAPollingInterval = 5000; // 5 seconds + + // File server name + + private String m_srvName; + + // Accept connections from any clients or the named client only + + private byte[] m_acceptClient; + + // Local NetBIOS name to listen for sessions on and assigned name number + + private NetBIOSName m_nbName; + private int m_nameNum; + + // Workstation NetBIOS name and assigned name number + + private NetBIOSName m_wksNbName; + private int m_wksNameNum; + + // NetBIOS LAN adapter to use + + private int m_lana = -1; + + // Flag to indicate if the LANA is valid or the network adapter is currently + // unplugged/offline/disabled + + private boolean m_lanaValid; + + // Polling interval in milliseconds to check if the configured LANA is back online + + private long m_lanaPoll; + + // Flag to indicate if we are using Win32 Netbios() or Winsock calls + + private boolean m_useWinsock; + + // Winsock Netbios socket to listen for incoming connections + + private NetBIOSSocket m_nbSocket; + + // Dummy socket used to register the workstation name that some clients search for, although they connect + // to the file server service + + private NetBIOSSocket m_wksSocket; + + /** + * Class constructor + * + * @param srv SMBServer + * @param debug boolean + */ + public Win32NetBIOSSessionSocketHandler(SMBServer srv, boolean debug) + { + super("Win32 NetBIOS", srv, debug); + + // Get the Win32 NetBIOS file server name + + if (srv.getConfiguration().getWin32ServerName() != null) + m_srvName = srv.getConfiguration().getWin32ServerName(); + else + m_srvName = srv.getConfiguration().getServerName(); + + // Get the accepted client string, defaults to '*' to accept any client connection + + NetBIOSName accName = new NetBIOSName("*", NetBIOSName.WorkStation, false); + m_acceptClient = accName.getNetBIOSName(); + + // Set the LANA to use, or -1 to use the first available + + m_lana = srv.getConfiguration().getWin32LANA(); + + // Set the Win32 NetBIOS code to use either the Netbios() API call or Winsock NetBIOS calls + + m_useWinsock = srv.getConfiguration().useWinsockNetBIOS(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS server " + m_srvName + " (using " + + (isUsingWinsock() ? "Winsock" : "Netbios() API") + ")"); + + // Set the LANA offline polling interval + + m_lanaPoll = LANAPollingInterval; + } + + /** + * Class constructor + * + * @param srv SMBServer + * @param lana int + * @param debug boolean + */ + public Win32NetBIOSSessionSocketHandler(SMBServer srv, int lana, boolean debug) + { + super("Win32 NetBIOS", srv, debug); + + // Get the Win32 NetBIOS file server name + + if (srv.getConfiguration().getWin32ServerName() != null) + m_srvName = srv.getConfiguration().getWin32ServerName(); + else + m_srvName = srv.getConfiguration().getServerName(); + + // Get the accepted client string, defaults to '*' to accept any client connection + + NetBIOSName accName = new NetBIOSName("*", NetBIOSName.WorkStation, false); + m_acceptClient = accName.getNetBIOSName(); + + // Set the LANA to use, or -1 to use the first available + + m_lana = lana; + + // Set the Win32 NetBIOS code to use either the Netbios() API call or Winsock NetBIOS calls + + m_useWinsock = srv.getConfiguration().useWinsockNetBIOS(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS server " + m_srvName + " (using " + + (isUsingWinsock() ? "Winsock" : "Netbios() API") + ")"); + + // Set the LANA offline polling interval + + m_lanaPoll = LANAPollingInterval; + } + + /** + * Class constructor + * + * @param srv SMBServer + * @param nbName String + * @param debug boolean + */ + public Win32NetBIOSSessionSocketHandler(SMBServer srv, String nbName, boolean debug) + { + super("Win32 NetBIOS", srv, debug); + + // Set the Win32 NetBIOS file server name + + m_srvName = nbName; + + // Get the accepted client string, defaults to '*' to accept any client connection + + NetBIOSName accName = new NetBIOSName("*", NetBIOSName.WorkStation, false); + m_acceptClient = accName.getNetBIOSName(); + + // Set the LANA to use, or -1 to use the first available + + m_lana = srv.getConfiguration().getWin32LANA(); + + // Set the Win32 NetBIOS code to use either the Netbios() API call or Winsock NetBIOS calls + + m_useWinsock = srv.getConfiguration().useWinsockNetBIOS(); + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS server " + m_srvName + " (using " + + (isUsingWinsock() ? "Winsock" : "Netbios() API") + ")"); + + // Set the LANA offline polling interval + + m_lanaPoll = LANAPollingInterval; + } + + /** + * Return the LANA number that is being used + * + * @return int + */ + public final int getLANANumber() + { + return m_lana; + } + + /** + * Return the LANA offline polling interval to check for the LANA coming back online + * + * @return long + */ + public final long getLANAOfflinePollingInterval() + { + return m_lanaPoll; + } + + /** + * Return the assigned NetBIOS name number + * + * @return int + */ + public final int getNameNumber() + { + return m_nameNum; + } + + /** + * Return the local server name + * + * @return String + */ + public final String getServerName() + { + return m_srvName; + } + + /** + * Determine if Netbios() API calls or Winsock calls are being used + * + * @return boolean + */ + public final boolean isUsingWinsock() + { + return m_useWinsock; + } + + /** + * Initialize the session socket handler. + * + * @throws Exception + */ + public void initialize() throws Exception + { + + // Enumerate the LAN adapters, use the first available if the LANA has not been specified in + // the configuration + + int[] lanas = Win32NetBIOS.LanaEnumerate(); + if (lanas != null && lanas.length > 0) + { + + // Check if the LANA has been specified via the configuration, if not then use the first + // available + + if (m_lana == -1) + m_lana = lanas[0]; + else + { + + // Check if the required LANA is available + + boolean lanaOnline = false; + int idx = 0; + + while (idx < lanas.length && lanaOnline == false) + { + + // Check if the LANA is listed + + if (lanas[idx++] == getLANANumber()) + lanaOnline = true; + } + + // If the LANA is not available the main listener thread will poll the available + // LANAs until the required LANA is available + + if (lanaOnline == false) + { + + // Indicate that the LANA is not offline/unplugged/disabled + + m_lanaValid = false; + return; + } + } + } + else + { + + // If the LANA has not been set throw an exception as no LANAs are available + + if (m_lana == -1) + throw new Exception("No Win32 NetBIOS LANAs available"); + + // The required LANA is offline/unplugged/disabled + + m_lanaValid = false; + return; + } + + // Create the local NetBIOS name to listen for incoming connections on + + m_nbName = new NetBIOSName(m_srvName, NetBIOSName.FileServer, false); + m_wksNbName = new NetBIOSName(m_srvName, NetBIOSName.WorkStation, false); + + // Initialize the Win32 NetBIOS interface, either Winsock or Netbios() API + + if ( isUsingWinsock()) + initializeWinsockNetBIOS(); + else + initializeNetbiosAPI(); + + // Indicate that the LANA is valid + + m_lanaValid = true; + } + + /** + * Initialize the Win32 Netbios() API interface, add the server names + * + * @exception Exception If the NetBIOS add name fails + */ + private final void initializeNetbiosAPI() + throws Exception + { + // Reset the LANA + + Win32NetBIOS.Reset(m_lana); + + // Add the NetBIOS name to the local name table + + m_nameNum = Win32NetBIOS.AddName(m_lana, m_nbName.getNetBIOSName()); + if (m_nameNum < 0) + throw new Exception("Win32 NetBIOS AddName failed (file server), status = 0x" + + Integer.toHexString(-m_nameNum) + ", " + NetBIOS.getErrorString(-m_nameNum)); + + // Register a NetBIOS name for the server name with the workstation name type, some clients + // use this name to find the server + + m_wksNameNum = Win32NetBIOS.AddName(m_lana, m_wksNbName.getNetBIOSName()); + if (m_wksNameNum < 0) + throw new Exception("Win32 NetBIOS AddName failed (workstation), status = 0x" + + Integer.toHexString(-m_wksNameNum) + ", " + NetBIOS.getErrorString(-m_wksNameNum)); + } + + /** + * Initialize the Winsock NetBIOS interface + * + * @exception Exception If a Winsock error occurs + */ + private final void initializeWinsockNetBIOS() + throws Exception + { + // Create the NetBIOS listener socket, this will add the file server name + + m_nbSocket = NetBIOSSocket.createListenerSocket( getLANANumber(), m_nbName); + + // Create a NetBIOS socket using the workstation name, some clients search for this name + + m_wksSocket = NetBIOSSocket.createListenerSocket( getLANANumber(), m_wksNbName); + } + + /** + * Check if the LANA is valid and accepting incoming sessions or the associated network adapter + * is unplugged/disabled/offline. + * + * @return boolean + */ + public final boolean isLANAValid() + { + return m_lanaValid; + } + + /** + * Shutdown the Win32 NetBIOS interface + */ + public void shutdownRequest() + { + super.shutdownRequest(); + + // Reset the LANA, if valid, to wake the main session listener thread + + if ( isLANAValid()) + Win32NetBIOS.Reset(m_lana); + + // If Winsock calls are being used close the sockets + + if ( isUsingWinsock()) + { + if ( m_nbSocket != null) + { + m_nbSocket.closeSocket(); + m_nbSocket = null; + } + + if ( m_wksSocket != null) + { + m_wksSocket.closeSocket(); + m_wksSocket = null; + } + } + } + + /** + * Run the NetBIOS session socket handler + */ + public void run() + { + + try + { + + // Clear the shutdown flag + + clearShutdown(); + + // Wait for incoming connection requests + + while (hasShutdown() == false) + { + + // Check if the LANA is valid and ready to accept incoming sessions + + if (isLANAValid()) + { + // Wait for an incoming session request + + if ( isUsingWinsock()) + { + // Wait for an incoming session request using the Winsock NetBIOS interface + + runWinsock(); + } + else + { + // Wait for an incoming session request using the Win32 Netbios() API interface + + runNetBIOS(); + } + } + else + { + + // Sleep for a short while ... + + try + { + Thread.sleep(getLANAOfflinePollingInterval()); + } + catch (Exception ex) + { + } + + // Check if the network adapter/LANA is back online, if so then re-initialize + // the LANA to start accepting sessions again + + try + { + initialize(); + } + catch (Exception ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + { + logger.debug("[SMB] Win32 NetBIOS Failed To ReInitialize LANA"); + logger.debug(" " + ex.getMessage()); + } + } + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug() && isLANAValid()) + logger.debug("[SMB] Win32 NetBIOS LANA " + getLANANumber() + " Back Online"); + } + } + } + catch (Exception ex) + { + + // Do not report an error if the server has shutdown, closing the server socket + // causes an exception to be thrown. + + if (hasShutdown() == false) + { + logger.debug("[SMB] Win32 NetBIOS Server error : " + ex.toString()); + logger.debug(ex); + } + } + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS session handler closed"); + } + + /** + * Run the Win32 Netbios() API listen code + * + * @exception Exception If an unhandled error occurs + */ + private final void runNetBIOS() + throws Exception + { + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Waiting for Win32 NetBIOS session request (Netbios API) ..."); + + // Clear the caller name + + byte[] callerNameBuf = new byte[NetBIOS.NCBNameSize]; + String callerName = null; + + callerNameBuf[0] = '\0'; + callerName = null; + + // Wait for a new NetBIOS session + + int lsn = Win32NetBIOS.Listen(m_lana, m_nbName.getNetBIOSName(), m_acceptClient, callerNameBuf); + + // Check if the session listener has been shutdown + + if ( hasShutdown()) + return; + + // Get the caller name, if available + + if (callerNameBuf[0] != '\0') + callerName = new String(callerNameBuf).trim(); + else + callerName = ""; + + // Create a packet handler and thread for the new session + + if (lsn >= 0) + { + + // Create a new session thread + + try + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS session request received, lsn=" + lsn + ", caller=[" + + callerName + "]"); + + // Create a packet handler for the session + + PacketHandler pktHandler = new Win32NetBIOSPacketHandler(m_lana, lsn, callerName); + + // Create a server session for the new request, and set the session id. + + SMBSrvSession srvSess = new SMBSrvSession(pktHandler, getServer()); + srvSess.setSessionId(getNextSessionId()); + srvSess.setUniqueId(pktHandler.getShortName() + srvSess.getSessionId()); + srvSess.setDebugPrefix("[" + pktHandler.getShortName() + srvSess.getSessionId() + "] "); + + // Add the session to the active session list + + getServer().addSession(srvSess); + + // Start the new session in a seperate thread + + Thread srvThread = new Thread(srvSess); + srvThread.setDaemon(true); + srvThread.setName("Sess_W" + srvSess.getSessionId() + "_LSN" + lsn); + srvThread.start(); + } + catch (Exception ex) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS Failed to create session, " + ex.toString()); + } + } + else + { + + // Check if the error indicates the network adapter is + // unplugged/offline/disabled + + int sts = -lsn; + + if (sts == NetBIOS.NRC_Bridge) + { + + // Indicate that the LANA is no longer valid + + m_lanaValid = false; + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS LANA offline/disabled, LANA=" + getLANANumber()); + } + else if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Win32 NetBIOS Listen error, 0x" + Integer.toHexString(-lsn) + ", " + + NetBIOS.getErrorString(-lsn)); + } + } + + /** + * Run the Winsock NetBIOS listen code + * + * @exception Exception If an unhandled error occurs + */ + private final void runWinsock() + throws Exception + { + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Waiting for Win32 NetBIOS session request (Winsock) ..."); + + // Wait for a new NetBIOS session + + NetBIOSSocket sessSock = null; + + try + { + // Wait for an incoming session connection + + sessSock = m_nbSocket.listen(); + } + catch ( WinsockNetBIOSException ex) + { + // Check if the network is down + + if ( ex.getErrorCode() == WinsockError.WsaENetDown) + { + // Check if the LANA we are listening on is no longer valid + + if ( isLANAOnline(m_lana) == false) + { + // Network/LANA is offline, cleanup the current listening sockets and wait for the + // LANA to come back online + + if ( m_nbSocket != null) + { + m_nbSocket.closeSocket(); + m_nbSocket = null; + } + + if ( m_wksSocket != null) + { + m_wksSocket.closeSocket(); + m_wksSocket = null; + } + + // Indciate that the LANA is no longer valid + + m_lanaValid = false; + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Winsock NetBIOS network down, LANA=" + m_lana); + } + } + else + { + // Debug + + if (hasShutdown() == false && logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Winsock NetBIOS listen error, " + ex.getMessage()); + } + } + + // Check if the session listener has been shutdown + + if ( hasShutdown()) + return; + + // Create a packet handler and thread for the new session + + if (sessSock != null) + { + + // Create a new session thread + + try + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Winsock NetBIOS session request received, caller=" + + sessSock.getName()); + + // Create a packet handler for the session + + PacketHandler pktHandler = new WinsockNetBIOSPacketHandler(m_lana, sessSock); + + // Create a server session for the new request, and set the session id. + + SMBSrvSession srvSess = new SMBSrvSession(pktHandler, getServer()); + srvSess.setSessionId(getNextSessionId()); + srvSess.setUniqueId(pktHandler.getShortName() + srvSess.getSessionId()); + srvSess.setDebugPrefix("[" + pktHandler.getShortName() + srvSess.getSessionId() + "] "); + + // Add the session to the active session list + + getServer().addSession(srvSess); + + // Start the new session in a seperate thread + + Thread srvThread = new Thread(srvSess); + srvThread.setDaemon(true); + srvThread.setName("Sess_WS" + srvSess.getSessionId()); + srvThread.start(); + } + catch (Exception ex) + { + + // Debug + + if (logger.isDebugEnabled() && hasDebug()) + logger.debug("[SMB] Winsock NetBIOS Failed to create session, " + ex.toString()); + } + } + } + + /** + * Create the Win32 NetBIOS session socket handlers for the main SMB/CIFS server + * + * @param server SMBServer + * @param sockDbg boolean + */ + public final static void createSessionHandlers(SMBServer server, boolean sockDbg) + { + + // Access the server configuration + + ServerConfiguration config = server.getConfiguration(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + { + int[] lanas = Win32NetBIOS.LanaEnumerate(); + + StringBuilder lanaStr = new StringBuilder(); + if (lanas != null && lanas.length > 0) + { + for (int i = 0; i < lanas.length; i++) + { + lanaStr.append(Integer.toString(lanas[i])); + lanaStr.append(" "); + } + } + logger.debug("[SMB] Win32 NetBIOS Available LANAs: " + lanaStr.toString()); + } + + // Check if the Win32 NetBIOS session handler should use a particular LANA/network adapter + // or should use all available LANAs/network adapters (that have NetBIOS enabled). + + Win32NetBIOSSessionSocketHandler sessHandler = null; + List lanaListeners = new ArrayList(); + + if (config.getWin32LANA() != -1) + { + + // Create a single Win32 NetBIOS session handler using the specified LANA + + sessHandler = new Win32NetBIOSSessionSocketHandler(server, config.getWin32LANA(), sockDbg); + + try + { + sessHandler.initialize(); + } + catch (Exception ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + { + logger.debug("[SMB] Win32 NetBIOS failed to create session handler for LANA " + + config.getWin32LANA()); + logger.debug(" " + ex.getMessage()); + } + } + + // Add the session handler to the SMB/CIFS server + + server.addSessionHandler(sessHandler); + + // Run the NetBIOS session handler in a seperate thread + + Thread nbThread = new Thread(sessHandler); + nbThread.setName("Win32NB_Handler_" + config.getWin32LANA()); + nbThread.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] Win32 NetBIOS created session handler on LANA " + config.getWin32LANA()); + + // Check if a host announcer should be enabled + + if (config.hasWin32EnableAnnouncer()) + { + + // Create a host announcer + + HostAnnouncer hostAnnouncer = null; + + String domain = config.getDomainName(); + int intvl = config.getWin32HostAnnounceInterval(); + + if ( config.useWinsockNetBIOS()) + { + // Create a Winsock NetBIOS announcer + + hostAnnouncer = new WinsockNetBIOSHostAnnouncer(sessHandler, domain, intvl); + } + else + { + // Create a Win32 Netbios() API announcer + + hostAnnouncer = new Win32NetBIOSHostAnnouncer(sessHandler, domain, intvl); + } + + // Enable announcer debug + + hostAnnouncer.setDebug(sockDbg); + + // Add the host announcer to the SMB/CIFS server list + + server.addHostAnnouncer(hostAnnouncer); + hostAnnouncer.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] Win32 NetBIOS host announcer enabled on LANA " + config.getWin32LANA()); + } + + // Check if the session handler implements the LANA listener interface + + if ( sessHandler instanceof LanaListener) + lanaListeners.add( sessHandler); + } + else + { + + // Get a list of the available LANAs + + int[] lanas = Win32NetBIOS.LanaEnumerate(); + + if (lanas != null && lanas.length > 0) + { + + // Create a session handler for each available LANA + + for (int i = 0; i < lanas.length; i++) + { + + // Get the current LANA + + int lana = lanas[i]; + + // Create a session handler + + sessHandler = new Win32NetBIOSSessionSocketHandler(server, lana, sockDbg); + + try + { + sessHandler.initialize(); + } + catch (Exception ex) + { + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + { + logger.debug("[SMB] Win32 NetBIOS failed to create session handler for LANA " + lana); + logger.debug(" " + ex.getMessage()); + } + } + + // Add the session handler to the SMB/CIFS server + + server.addSessionHandler(sessHandler); + + // Run the NetBIOS session handler in a seperate thread + + Thread nbThread = new Thread(sessHandler); + nbThread.setName("Win32NB_Handler_" + lana); + nbThread.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] Win32 NetBIOS created session handler on LANA " + lana); + + // Check if a host announcer should be enabled + + if (config.hasWin32EnableAnnouncer()) + { + + // Create a host announcer + + HostAnnouncer hostAnnouncer = null; + + String domain = config.getDomainName(); + int intvl = config.getWin32HostAnnounceInterval(); + + if ( config.useWinsockNetBIOS()) + { + // Create a Winsock NetBIOS announcer + + hostAnnouncer = new WinsockNetBIOSHostAnnouncer(sessHandler, domain, intvl); + } + else + { + // Create a Win32 Netbios() API announcer + + hostAnnouncer = new Win32NetBIOSHostAnnouncer(sessHandler, domain, intvl); + } + + // Enable announcer debug + + hostAnnouncer.setDebug(sockDbg); + + // Add the host announcer to the SMB/CIFS server list + + server.addHostAnnouncer(hostAnnouncer); + hostAnnouncer.start(); + + // DEBUG + + if (logger.isDebugEnabled() && sockDbg) + logger.debug("[SMB] Win32 NetBIOS host announcer enabled on LANA " + lana); + } + + // Check if the session handler implements the LANA listener interface + + if ( sessHandler instanceof LanaListener) + lanaListeners.add( sessHandler); + } + } + + // Create a LANA monitor to check for new LANAs becoming available + + Win32NetBIOSLanaMonitor lanaMonitor = new Win32NetBIOSLanaMonitor(server, lanas, LANAPollingInterval, sockDbg); + + // Register any session handlers that are LANA listeners + + if ( lanaListeners.size() > 0) + { + for ( Win32NetBIOSSessionSocketHandler handler : lanaListeners) + { + // Register the LANA listener + + lanaMonitor.addLanaListener( handler.getLANANumber(), handler); + } + } + } + } + + /** + * Check if the specified LANA is online + * + * @param lana int + * @return boolean + */ + private final boolean isLANAOnline(int lana) + { + // Get a list of the available LANAs + + int[] lanas = Win32NetBIOS.LanaEnumerate(); + + if (lanas != null && lanas.length > 0) + { + // Check if the specified LANA is available + + for (int i = 0; i < lanas.length; i++) + { + if ( lanas[i] == lana) + return true; + } + } + + // LANA not online + + return false; + } + + /** + * LANA listener status change callback + * + * @param lana int + * @param online boolean + */ + public void lanaStatusChange(int lana, boolean online) + { + // If the LANA has gone offline, close the listening socket and wait for the LANA to + // come back online + + if ( online == false) + { + // Indicate that the LANA is offline + + m_lanaValid = false; + + // Close the listening sockets + + if ( m_nbSocket != null) + { + m_nbSocket.closeSocket(); + m_nbSocket = null; + } + + if ( m_wksSocket != null) + { + m_wksSocket.closeSocket(); + m_wksSocket = null; + } + } + } +} diff --git a/source/java/org/alfresco/filesys/smb/server/win32/WinsockNetBIOSPacketHandler.java b/source/java/org/alfresco/filesys/smb/server/win32/WinsockNetBIOSPacketHandler.java new file mode 100644 index 0000000000..4154f188da --- /dev/null +++ b/source/java/org/alfresco/filesys/smb/server/win32/WinsockNetBIOSPacketHandler.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.smb.server.win32; + +import java.io.IOException; + +import org.alfresco.filesys.netbios.RFCNetBIOSProtocol; +import org.alfresco.filesys.netbios.win32.NetBIOSSocket; +import org.alfresco.filesys.netbios.win32.WinsockError; +import org.alfresco.filesys.netbios.win32.WinsockNetBIOSException; +import org.alfresco.filesys.smb.server.PacketHandler; +import org.alfresco.filesys.smb.server.SMBSrvPacket; + +/** + * Winsock NetBIOS Packet Handler Class + * + *

    Uses a Windows Winsock NetBIOS socket to provide the low level session layer for better integration + * with Windows. + * + * @author GKSpencer + */ +public class WinsockNetBIOSPacketHandler extends PacketHandler +{ + // Constants + // + // Receive error indicating a receive buffer error + + private static final int ReceiveBufferSizeError = 0x80000000; + + // Network LAN adapter to use + + private int m_lana; + + // NetBIOS session socket + + private NetBIOSSocket m_sessSock; + + /** + * Class constructor + * + * @param lana int + * @param sock NetBIOSSocket + */ + public WinsockNetBIOSPacketHandler(int lana, NetBIOSSocket sock) + { + super(SMBSrvPacket.PROTOCOL_WIN32NETBIOS, "WinsockNB", "WSNB", sock.getName().getName()); + + m_lana = lana; + m_sessSock = sock; + } + + /** + * Return the LANA number + * + * @return int + */ + public final int getLANA() + { + return m_lana; + } + + /** + * Return the NetBIOS socket + * + * @return NetBIOSSocket + */ + public final NetBIOSSocket getSocket() + { + return m_sessSock; + } + + /** + * Read a packet from the client + * + * @param pkt SMBSrvPacket + * @return int + * @throws IOException + */ + public int readPacket(SMBSrvPacket pkt) throws IOException + { + // Receive an SMB/CIFS request packet via the Winsock NetBIOS socket + + int rxlen = 0; + + try { + + // Read a pakcet of data + + rxlen = m_sessSock.read(pkt.getBuffer(), 4, pkt.getBufferLength() - 4); + + // Check if the buffer is not big enough to receive the entire packet, extend the buffer + // and read the remaining part of the packet + + if (rxlen == ReceiveBufferSizeError) + { + + // Check if the packet buffer is already at the maximum size (we assume the maximum + // size is the maximum that RFC NetBIOS can send which is 17bits) + + if (pkt.getBuffer().length < RFCNetBIOSProtocol.MaxPacketSize) + { + // Set the initial receive size, assume a full read + + rxlen = pkt.getBufferLength() - 4; + + // Allocate a new buffer, copy the existing data to the new buffer + + byte[] newbuf = new byte[RFCNetBIOSProtocol.MaxPacketSize]; + System.arraycopy(pkt.getBuffer(), 4, newbuf, 4, rxlen); + pkt.setBuffer( newbuf); + + // Receive the packet + + int rxlen2 = m_sessSock.read(pkt.getBuffer(), rxlen + 4, pkt.getBufferLength() - (rxlen + 4)); + + if ( rxlen2 == ReceiveBufferSizeError) + throw new WinsockNetBIOSException(WinsockError.WsaEMsgSize); + + rxlen += rxlen2; + } + else + throw new WinsockNetBIOSException(WinsockError.WsaEMsgSize); + } + } + catch ( WinsockNetBIOSException ex) + { + // Check if the remote client has closed the socket + + if ( ex.getErrorCode() == WinsockError.WsaEConnReset) + { + // Indicate that the socket has been closed + + rxlen = -1; + } + else + { + // Rethrow the exception + + throw ex; + } + } + + // Return the received packet length + + return rxlen; + } + + /** + * Write a packet to the client + * + * @param pkt SMBSrvPacket + * @param len int + * @throws IOException + */ + public void writePacket(SMBSrvPacket pkt, int len) throws IOException + { + // Output the packet via the Winsock NetBIOS socket + // + // As Windows is handling the NetBIOS session layer we do not send the 4 byte header that is + // used by the NetBIOS over TCP/IP and native SMB packet handlers. + + int txlen = m_sessSock.write(pkt.getBuffer(), 4, len); + + // Do not check the status, if the session has been closed the next receive will fail + } + + /** + * Close the Winsock NetBIOS packet handler. + */ + public void closeHandler() + { + super.closeHandler(); + + // Close the session socket + + if ( m_sessSock != null) + m_sessSock.closeSocket(); + } +} diff --git a/source/java/org/alfresco/filesys/util/DataBuffer.java b/source/java/org/alfresco/filesys/util/DataBuffer.java new file mode 100644 index 0000000000..bc93af5e0a --- /dev/null +++ b/source/java/org/alfresco/filesys/util/DataBuffer.java @@ -0,0 +1,847 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +/** + * Data Buffer Class + *

    + * Dynamic buffer for getting/setting data blocks. + */ +public class DataBuffer +{ + + // Constants + + private static final int DefaultBufferSize = 256; + + // Data buffer, current position and offset + + private byte[] m_data; + private int m_pos; + private int m_endpos; + private int m_offset; + + /** + * Default constructor + */ + public DataBuffer() + { + m_data = new byte[DefaultBufferSize]; + m_pos = 0; + m_offset = 0; + } + + /** + * Create a data buffer to write data to + * + * @param siz int + */ + public DataBuffer(int siz) + { + m_data = new byte[siz]; + m_pos = 0; + m_offset = 0; + } + + /** + * Create a data buffer to read data from + * + * @param buf byte[] + * @param off int + * @param len int + */ + public DataBuffer(byte[] buf, int off, int len) + { + m_data = buf; + m_offset = off; + m_pos = off; + m_endpos = off + len; + } + + /** + * Return the data buffer + * + * @return byte[] + */ + public final byte[] getBuffer() + { + return m_data; + } + + /** + * Return the data length + * + * @return int + */ + public final int getLength() + { + if (m_endpos != 0) + return m_endpos - m_offset; + return m_pos - m_offset; + } + + /** + * Return the data length in words + * + * @return int + */ + public final int getLengthInWords() + { + return getLength() / 2; + } + + /** + * Return the available data length + * + * @return int + */ + public final int getAvailableLength() + { + if (m_endpos == 0) + return -1; + return m_endpos - m_pos; + } + + /** + * Return the displacement from the start of the buffer to the current buffer position + * + * @return int + */ + public final int getDisplacement() + { + return m_pos - m_offset; + } + + /** + * Return the buffer base offset + * + * @return int + */ + public final int getOffset() + { + return m_offset; + } + + /** + * Get a byte from the buffer + * + * @return int + */ + public final int getByte() + { + + // Check if there is enough data in the buffer + + if (m_data.length - m_pos < 1) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the byte value + + int bval = (int) (m_data[m_pos] & 0xFF); + m_pos++; + return bval; + } + + /** + * Get a short from the buffer + * + * @return int + */ + public final int getShort() + { + + // Check if there is enough data in the buffer + + if (m_data.length - m_pos < 2) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the integer value + + int sval = (int) DataPacker.getIntelShort(m_data, m_pos); + m_pos += 2; + return sval; + } + + /** + * Get an integer from the buffer + * + * @return int + */ + public final int getInt() + { + + // Check if there is enough data in the buffer + + if (m_data.length - m_pos < 4) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the integer value + + int ival = DataPacker.getIntelInt(m_data, m_pos); + m_pos += 4; + return ival; + } + + /** + * Get a long (64 bit) value from the buffer + * + * @return long + */ + public final long getLong() + { + + // Check if there is enough data in the buffer + + if (m_data.length - m_pos < 8) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the long value + + long lval = DataPacker.getIntelLong(m_data, m_pos); + m_pos += 8; + return lval; + } + + /** + * Get a string from the buffer + * + * @param uni boolean + * @return String + */ + public final String getString(boolean uni) + { + return getString(255, uni); + } + + /** + * Get a string from the buffer + * + * @param maxlen int + * @param uni boolean + * @return String + */ + public final String getString(int maxlen, boolean uni) + { + + // Check for Unicode or ASCII + + String ret = null; + int availLen = -1; + + if (uni) + { + + // Word align the current buffer position, calculate the available + // length + + m_pos = DataPacker.wordAlign(m_pos); + availLen = (m_endpos - m_pos) / 2; + if (availLen < maxlen) + maxlen = availLen; + + ret = DataPacker.getUnicodeString(m_data, m_pos, maxlen); + if (ret != null) + m_pos += (ret.length() * 2) + 2; + } + else + { + + // Calculate the available length + + availLen = m_endpos - m_pos; + if (availLen < maxlen) + maxlen = availLen; + + // Unpack the ASCII string + + ret = DataPacker.getString(m_data, m_pos, maxlen); + if (ret != null) + m_pos += ret.length() + 1; + } + + // Return the string + + return ret; + } + + /** + * Get a short from the buffer at the specified index + * + * @param idx int + * @return int + */ + public final int getShortAt(int idx) + { + + // Check if there is enough data in the buffer + + int pos = m_offset + (idx * 2); + if (m_data.length - pos < 2) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the integer value + + int sval = (int) DataPacker.getIntelShort(m_data, pos) & 0xFFFF; + return sval; + } + + /** + * Get an integer from the buffer at the specified index + * + * @param idx int + * @return int + */ + public final int getIntAt(int idx) + { + + // Check if there is enough data in the buffer + + int pos = m_offset + (idx * 2); + if (m_data.length - pos < 4) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the integer value + + int ival = DataPacker.getIntelInt(m_data, pos); + return ival; + } + + /** + * Get a long (64 bit) value from the buffer at the specified index + * + * @param idx int + * @return long + */ + public final long getLongAt(int idx) + { + + // Check if there is enough data in the buffer + + int pos = m_offset + (idx * 2); + if (m_data.length - pos < 8) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Unpack the long value + + long lval = DataPacker.getIntelLong(m_data, pos); + return lval; + } + + /** + * Skip over a number of bytes + * + * @param cnt int + */ + public final void skipBytes(int cnt) + { + + // Check if there is enough data in the buffer + + if (m_data.length - m_pos < cnt) + throw new ArrayIndexOutOfBoundsException("End of data buffer"); + + // Skip bytes + + m_pos += cnt; + } + + /** + * Return the data position + * + * @return int + */ + public final int getPosition() + { + return m_pos; + } + + /** + * Set the read/write buffer position + * + * @param pos int + */ + public final void setPosition(int pos) + { + m_pos = pos; + } + + /** + * Set the end of buffer position, and reset the read position to the beginning of the buffer + */ + public final void setEndOfBuffer() + { + m_endpos = m_pos; + m_pos = m_offset; + } + + /** + * Set the data length + * + * @param len int + */ + public final void setLength(int len) + { + m_pos = m_offset + len; + } + + /** + * Append a byte value to the buffer + * + * @param bval int + */ + public final void putByte(int bval) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < 1) + extendBuffer(); + + // Pack the byte value + + m_data[m_pos++] = (byte) (bval & 0xFF); + } + + /** + * Append a short value to the buffer + * + * @param sval int + */ + public final void putShort(int sval) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < 2) + extendBuffer(); + + // Pack the short value + + DataPacker.putIntelShort(sval, m_data, m_pos); + m_pos += 2; + } + + /** + * Append an integer to the buffer + * + * @param ival int + */ + public final void putInt(int ival) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < 4) + extendBuffer(); + + // Pack the integer value + + DataPacker.putIntelInt(ival, m_data, m_pos); + m_pos += 4; + } + + /** + * Append a long to the buffer + * + * @param lval long + */ + public final void putLong(long lval) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < 8) + extendBuffer(); + + // Pack the long value + + DataPacker.putIntelLong(lval, m_data, m_pos); + m_pos += 8; + } + + /** + * Append a short value to the buffer at the specified index + * + * @param idx int + * @param sval int + */ + public final void putShortAt(int idx, int sval) + { + + // Check if there is enough space in the buffer + + int pos = m_offset + (idx * 2); + if (m_data.length - pos < 2) + extendBuffer(); + + // Pack the short value + + DataPacker.putIntelShort(sval, m_data, pos); + } + + /** + * Append an integer to the buffer at the specified index + * + * @param idx int + * @param ival int + */ + public final void putIntAt(int idx, int ival) + { + + // Check if there is enough space in the buffer + + int pos = m_offset = (idx * 2); + if (m_data.length - pos < 4) + extendBuffer(); + + // Pack the integer value + + DataPacker.putIntelInt(ival, m_data, pos); + } + + /** + * Append a long to the buffer at the specified index + * + * @param idx int + * @param lval long + */ + public final void putLongAt(int idx, int lval) + { + + // Check if there is enough space in the buffer + + int pos = m_offset = (idx * 2); + if (m_data.length - pos < 8) + extendBuffer(); + + // Pack the long value + + DataPacker.putIntelLong(lval, m_data, pos); + } + + /** + * Append a string to the buffer + * + * @param str String + * @param uni boolean + */ + public final void putString(String str, boolean uni) + { + putString(str, uni, true); + } + + /** + * Append a string to the buffer + * + * @param str String + * @param uni boolean + * @param nulTerm boolean + */ + public final void putString(String str, boolean uni, boolean nulTerm) + { + + // Check for Unicode or ASCII + + if (uni) + { + + // Check if there is enough space in the buffer + + int bytLen = str.length() * 2; + if (m_data.length - m_pos < bytLen) + extendBuffer(bytLen + 4); + + // Word align the buffer position, pack the Unicode string + + m_pos = DataPacker.wordAlign(m_pos); + DataPacker.putUnicodeString(str, m_data, m_pos, nulTerm); + m_pos += (str.length() * 2); + if (nulTerm) + m_pos += 2; + } + else + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < str.length()) + extendBuffer(str.length() + 2); + + // Pack the ASCII string + + DataPacker.putString(str, m_data, m_pos, nulTerm); + m_pos += str.length(); + if (nulTerm) + m_pos++; + } + } + + /** + * Append a fixed length string to the buffer + * + * @param str String + * @param len int + */ + public final void putFixedString(String str, int len) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < str.length()) + extendBuffer(str.length() + 2); + + // Pack the ASCII string + + DataPacker.putString(str, len, m_data, m_pos); + m_pos += len; + } + + /** + * Append a string to the buffer at the specified buffer position + * + * @param str String + * @param pos int + * @param uni boolean + * @param nulTerm boolean + * @return int + */ + public final int putStringAt(String str, int pos, boolean uni, boolean nulTerm) + { + + // Check for Unicode or ASCII + + int retPos = -1; + + if (uni) + { + + // Check if there is enough space in the buffer + + int bytLen = str.length() * 2; + if (m_data.length - pos < bytLen) + extendBuffer(bytLen + 4); + + // Word align the buffer position, pack the Unicode string + + pos = DataPacker.wordAlign(pos); + retPos = DataPacker.putUnicodeString(str, m_data, pos, nulTerm); + } + else + { + + // Check if there is enough space in the buffer + + if (m_data.length - pos < str.length()) + extendBuffer(str.length() + 2); + + // Pack the ASCII string + + retPos = DataPacker.putString(str, m_data, pos, nulTerm); + } + + // Return the end of string buffer position + + return retPos; + } + + /** + * Append a fixed length string to the buffer at the specified position + * + * @param str String + * @param len int + * @param pos int + * @return int + */ + public final int putFixedStringAt(String str, int len, int pos) + { + + // Check if there is enough space in the buffer + + if (m_data.length - pos < str.length()) + extendBuffer(str.length() + 2); + + // Pack the ASCII string + + return DataPacker.putString(str, len, m_data, pos); + } + + /** + * Append a string pointer to the specified buffer offset + * + * @param off int + */ + public final void putStringPointer(int off) + { + + // Calculate the offset from the start of the data buffer to the string + // position + + DataPacker.putIntelInt(off - m_offset, m_data, m_pos); + m_pos += 4; + } + + /** + * Append zero bytes to the buffer + * + * @param cnt int + */ + public final void putZeros(int cnt) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < cnt) + extendBuffer(cnt); + + // Pack the zero bytes + + for (int i = 0; i < cnt; i++) + m_data[m_pos++] = 0; + } + + /** + * Word align the buffer position + */ + public final void wordAlign() + { + m_pos = DataPacker.wordAlign(m_pos); + } + + /** + * Longword align the buffer position + */ + public final void longwordAlign() + { + m_pos = DataPacker.longwordAlign(m_pos); + } + + /** + * Append a raw data block to the data buffer + * + * @param buf byte[] + * @param off int + * @param len int + */ + public final void appendData(byte[] buf, int off, int len) + { + + // Check if there is enough space in the buffer + + if (m_data.length - m_pos < len) + extendBuffer(len); + + // Copy the data to the buffer and update the current write position + + System.arraycopy(buf, off, m_data, m_pos, len); + m_pos += len; + } + + /** + * Copy all data from the data buffer to the user buffer, and update the read position + * + * @param buf byte[] + * @param off int + * @return int + */ + public final int copyData(byte[] buf, int off) + { + return copyData(buf, off, getLength()); + } + + /** + * Copy data from the data buffer to the user buffer, and update the current read position. + * + * @param buf byte[] + * @param off int + * @param cnt int + * @return int + */ + public final int copyData(byte[] buf, int off, int cnt) + { + + // Check if there is any more data to copy + + if (m_pos == m_endpos) + return 0; + + // Calculate the amount of data to copy + + int siz = m_endpos - m_pos; + if (siz > cnt) + siz = cnt; + + // Copy the data to the user buffer and update the current read position + + System.arraycopy(m_data, m_pos, buf, off, siz); + m_pos += siz; + + // Return the amount of data copied + + return siz; + } + + /** + * Extend the data buffer by the specified amount + * + * @param ext int + */ + private final void extendBuffer(int ext) + { + + // Create a new buffer of the required size + + byte[] newBuf = new byte[m_data.length + ext]; + + // Copy the data from the current buffer to the new buffer + + System.arraycopy(m_data, 0, newBuf, 0, m_data.length); + + // Set the new buffer to be the main buffer + + m_data = newBuf; + } + + /** + * Extend the data buffer, double the currently allocated buffer size + */ + private final void extendBuffer() + { + extendBuffer(m_data.length * 2); + } + + /** + * Return the data buffer details as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + + str.append("[data="); + str.append(m_data); + str.append(","); + str.append(m_pos); + str.append("/"); + str.append(m_offset); + str.append("/"); + str.append(getLength()); + str.append("]"); + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/util/DataPacker.java b/source/java/org/alfresco/filesys/util/DataPacker.java new file mode 100644 index 0000000000..1be1f92a6b --- /dev/null +++ b/source/java/org/alfresco/filesys/util/DataPacker.java @@ -0,0 +1,778 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +/** + * The data packing class is a static class that is used to pack and unpack basic data types to/from + * network byte order and Intel byte order. + */ +public final class DataPacker +{ + + // Flag to indicate the byte order of the platform that we are currently + // running on. + + private static boolean bigEndian = false; + + /** + * Return the current endian setting. + * + * @return true if the system is big endian, else false. + */ + public final static boolean isBigEndian() + { + return bigEndian; + } + + /** + * Unpack a null terminated data string from the data buffer. + * + * @param typ Data type, as specified by SMBDataType. + * @param bytarray Byte array to unpack the string value from. + * @param pos Offset to start unpacking the string value. + * @param maxlen Maximum length of data to be searched for a null character. + * @param uni String is Unicode if true, else ASCII + * @return String, else null if the terminating null character was not found. + */ + public final static String getDataString(char typ, byte[] bytarray, int pos, int maxlen, boolean uni) + { + + // Check if the data string has the required data type + + if (bytarray[pos++] == (byte) typ) + { + + // Extract the null terminated string + + if (uni == true) + return getUnicodeString(bytarray, wordAlign(pos), maxlen / 2); + else + return getString(bytarray, pos, maxlen - 1); + } + + // Invalid data type + + return null; + } + + /** + * Unpack a 32-bit integer. + * + * @param buf Byte buffer containing the integer to be unpacked. + * @param pos Position within the buffer that the integer is stored. + * @return The unpacked 32-bit integer value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static int getInt(byte[] buf, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough + + if (buf.length < pos + 3) + throw new java.lang.IndexOutOfBoundsException(); + + // Unpack the 32-bit value + + int i1 = (int) buf[pos] & 0xFF; + int i2 = (int) buf[pos + 1] & 0xFF; + int i3 = (int) buf[pos + 2] & 0xFF; + int i4 = (int) buf[pos + 3] & 0xFF; + + int iVal = (i1 << 24) + (i2 << 16) + (i3 << 8) + i4; + + // Return the unpacked value + + return iVal; + } + + /** + * Unpack a 32-bit integer that is stored in Intel format. + * + * @param bytarray Byte array containing the Intel integer to be unpacked. + * @param pos Offset that the Intel integer is stored within the byte array. + * @return Unpacked integer value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static int getIntelInt(byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to restore the int + + if (bytarray.length < pos + 3) + throw new java.lang.IndexOutOfBoundsException(); + + // Determine the byte ordering for this platform, and restore the int + + int iVal = 0; + + // Restore the int value from the byte array + + int i1 = (int) bytarray[pos + 3] & 0xFF; + int i2 = (int) bytarray[pos + 2] & 0xFF; + int i3 = (int) bytarray[pos + 1] & 0xFF; + int i4 = (int) bytarray[pos] & 0xFF; + + iVal = (i1 << 24) + (i2 << 16) + (i3 << 8) + i4; + + // Return the int value + + return iVal; + } + + /** + * Unpack a 64-bit long. + * + * @param buf Byte buffer containing the integer to be unpacked. + * @param pos Position within the buffer that the integer is stored. + * @return The unpacked 64-bit long value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static long getLong(byte[] buf, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to restore the long + + if (buf.length < pos + 7) + throw new java.lang.IndexOutOfBoundsException(); + + // Restore the long value from the byte array + + long lVal = 0L; + + for (int i = 0; i < 8; i++) + { + + // Get the current byte, shift the value and add to the return value + + long curVal = (long) buf[pos + i] & 0xFF; + curVal = curVal << ((7 - i) * 8); + lVal += curVal; + } + + // Return the long value + + return lVal; + } + + /** + * Unpack a 64-bit integer that is stored in Intel format. + * + * @param bytarray Byte array containing the Intel long to be unpacked. + * @param pos Offset that the Intel integer is stored within the byte array. + * @return Unpacked long value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static long getIntelLong(byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to restore the long + + if (bytarray.length < pos + 7) + throw new java.lang.IndexOutOfBoundsException(); + + // Restore the long value from the byte array + + long lVal = 0L; + + for (int i = 0; i < 8; i++) + { + + // Get the current byte, shift the value and add to the return value + + long curVal = (long) bytarray[pos + i] & 0xFF; + curVal = curVal << (i * 8); + lVal += curVal; + } + + // Return the long value + + return lVal; + } + + /** + * Unpack a 16-bit value that is stored in Intel format. + * + * @param bytarray Byte array containing the short value to be unpacked. + * @param pos Offset to start unpacking the short value. + * @return Unpacked short value. + * @exception java.lang.IndexOutOfBoiundsException If there is not enough data in the buffer. + */ + public final static int getIntelShort(byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to restore the int + + if (bytarray.length < pos) + throw new java.lang.IndexOutOfBoundsException(); + + // Restore the short value from the byte array + + int sVal = (((int) bytarray[pos + 1] << 8) + ((int) bytarray[pos] & 0xFF)); + + // Return the short value + + return sVal & 0xFFFF; + } + + /** + * Unpack a 16-bit value. + * + * @param bytarray Byte array containing the short to be unpacked. + * @param pos Offset within the byte array that the short is stored. + * @return Unpacked short value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static int getShort(byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to restore the int + + if (bytarray.length < pos) + throw new java.lang.IndexOutOfBoundsException(); + + // Determine the byte ordering for this platform, and restore the short + + int sVal = 0; + + if (bigEndian == true) + { + + // Big endian + + sVal = ((((int) bytarray[pos + 1]) << 8) + ((int) bytarray[pos] & 0xFF)); + } + else + { + + // Little endian + + sVal = ((((int) bytarray[pos]) << 8) + ((int) bytarray[pos + 1] & 0xFF)); + } + + // Return the short value + + return sVal & 0xFFFF; + } + + /** + * Unpack a null terminated string from the data buffer. + * + * @param bytarray Byte array to unpack the string value from. + * @param pos Offset to start unpacking the string value. + * @param maxlen Maximum length of data to be searched for a null character. + * @return String, else null if the terminating null character was not found. + */ + public final static String getString(byte[] bytarray, int pos, int maxlen) + { + + // Search for the trailing null + + int maxpos = pos + maxlen; + int endpos = pos; + + while (bytarray[endpos] != 0x00 && endpos < maxpos) + endpos++; + + // Check if we reached the end of the buffer + + if (endpos <= maxpos) + return new String(bytarray, pos, endpos - pos); + return null; + } + + /** + * Unpack a null terminated string from the data buffer. The string may be ASCII or Unicode. + * + * @param bytarray Byte array to unpack the string value from. + * @param pos Offset to start unpacking the string value. + * @param maxlen Maximum length of data to be searched for a null character. + * @param isUni Unicode string if true, else ASCII string + * @return String, else null if the terminating null character was not found. + */ + public final static String getString(byte[] bytarray, int pos, int maxlen, boolean isUni) + { + + // Get a string from the buffer + + String str = null; + + if (isUni) + str = getUnicodeString(bytarray, pos, maxlen); + else + str = getString(bytarray, pos, maxlen); + + // return the string + + return str; + } + + /** + * Unpack a null terminated Unicode string from the data buffer. + * + * @param byt Byte array to unpack the string value from. + * @param pos Offset to start unpacking the string value. + * @param maxlen Maximum length of data to be searched for a null character. + * @return String, else null if the terminating null character was not found. + */ + public final static String getUnicodeString(byte[] byt, int pos, int maxlen) + { + + // Check for an empty string + + if (maxlen == 0) + return ""; + + // Search for the trailing null + + int maxpos = pos + (maxlen * 2); + int endpos = pos; + char[] chars = new char[maxlen]; + int cpos = 0; + char curChar; + + do + { + + // Get a Unicode character from the buffer + + curChar = (char) (((byt[endpos + 1] & 0xFF) << 8) + (byt[endpos] & 0xFF)); + + // Add the character to the array + + chars[cpos++] = curChar; + + // Update the buffer pointer + + endpos += 2; + + } while (curChar != 0 && endpos < maxpos); + + // Check if we reached the end of the buffer + + if (endpos <= maxpos) + { + if (curChar == 0) + cpos--; + return new String(chars, 0, cpos); + } + return null; + } + + /** + * Pack a 32-bit integer into the supplied byte buffer. + * + * @param val Integer value to be packed. + * @param bytarray Byte buffer to pack the integer value into. + * @param pos Offset to start packing the integer value. + * @exception java.lang.IndexOutOfBoundsException If the buffer does not have enough space. + */ + public final static void putInt(int val, byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the int + + if (bytarray.length < pos + 3) + throw new java.lang.IndexOutOfBoundsException(); + + // Pack the integer value + + bytarray[pos] = (byte) ((val >> 24) & 0xFF); + bytarray[pos + 1] = (byte) ((val >> 16) & 0xFF); + bytarray[pos + 2] = (byte) ((val >> 8) & 0xFF); + bytarray[pos + 3] = (byte) (val & 0xFF); + } + + /** + * Pack an 32-bit integer value in Intel format. + * + * @param val Integer value to be packed. + * @param bytarray Byte array to pack the value into. + * @param pos Offset to start packing the integer value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static void putIntelInt(int val, byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the int + + if (bytarray.length < pos + 3) + throw new java.lang.IndexOutOfBoundsException(); + + // Store the int value in the byte array + + bytarray[pos + 3] = (byte) ((val >> 24) & 0xFF); + bytarray[pos + 2] = (byte) ((val >> 16) & 0xFF); + bytarray[pos + 1] = (byte) ((val >> 8) & 0xFF); + bytarray[pos] = (byte) (val & 0xFF); + } + + /** + * Pack a 64-bit integer value into the buffer + * + * @param val Integer value to be packed. + * @param bytarray Byte array to pack the value into. + * @param pos Offset to start packing the integer value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static void putLong(long val, byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the int + + if (bytarray.length < pos + 7) + throw new java.lang.IndexOutOfBoundsException(); + + // Store the long value in the byte array + + bytarray[pos] = (byte) ((val >> 56) & 0xFF); + bytarray[pos + 1] = (byte) ((val >> 48) & 0xFF); + bytarray[pos + 2] = (byte) ((val >> 40) & 0xFF); + bytarray[pos + 3] = (byte) ((val >> 32) & 0xFF); + bytarray[pos + 4] = (byte) ((val >> 24) & 0xFF); + bytarray[pos + 5] = (byte) ((val >> 16) & 0xFF); + bytarray[pos + 6] = (byte) ((val >> 8) & 0xFF); + bytarray[pos + 7] = (byte) (val & 0xFF); + } + + /** + * Pack a 64-bit integer value in Intel format. + * + * @param val Integer value to be packed. + * @param bytarray Byte array to pack the value into. + * @param pos Offset to start packing the integer value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static void putIntelLong(long val, byte[] bytarray, int pos) + throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the int + + if (bytarray.length < pos + 7) + throw new java.lang.IndexOutOfBoundsException(); + + // Store the long value in the byte array + + bytarray[pos + 7] = (byte) ((val >> 56) & 0xFF); + bytarray[pos + 6] = (byte) ((val >> 48) & 0xFF); + bytarray[pos + 5] = (byte) ((val >> 40) & 0xFF); + bytarray[pos + 4] = (byte) ((val >> 32) & 0xFF); + bytarray[pos + 3] = (byte) ((val >> 24) & 0xFF); + bytarray[pos + 2] = (byte) ((val >> 16) & 0xFF); + bytarray[pos + 1] = (byte) ((val >> 8) & 0xFF); + bytarray[pos] = (byte) (val & 0xFF); + } + + /** + * Pack a 64-bit integer value in Intel format. + * + * @param val Integer value to be packed. + * @param bytarray Byte array to pack the value into. + * @param pos Offset to start packing the integer value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static void putIntelLong(int val, byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the int + + if (bytarray.length < pos + 7) + throw new java.lang.IndexOutOfBoundsException(); + + // Store the int value in the byte array + + bytarray[pos + 7] = (byte) 0; + bytarray[pos + 6] = (byte) 0; + bytarray[pos + 5] = (byte) 0; + bytarray[pos + 4] = (byte) 0; + bytarray[pos + 3] = (byte) ((val >> 24) & 0xFF); + bytarray[pos + 2] = (byte) ((val >> 16) & 0xFF); + bytarray[pos + 1] = (byte) ((val >> 8) & 0xFF); + bytarray[pos] = (byte) (val & 0xFF); + } + + /** + * Pack a 16 bit value in Intel byte order. + * + * @param val Short value to be packed. + * @param bytarray Byte array to pack the short value into. + * @param pos Offset to start packing the short value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static void putIntelShort(int val, byte[] bytarray, int pos) + throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the short + + if (bytarray.length < pos) + throw new java.lang.IndexOutOfBoundsException(); + + // Pack the short value + + bytarray[pos + 1] = (byte) ((val >> 8) & 0xFF); + bytarray[pos] = (byte) (val & 0xFF); + } + + /** + * Pack a 16-bit value into the supplied byte buffer. + * + * @param val Short value to be packed. + * @param bytarray Byte array to pack the short value into. + * @param pos Offset to start packing the short value. + * @exception java.lang.IndexOutOfBoundsException If there is not enough data in the buffer. + */ + public final static void putShort(int val, byte[] bytarray, int pos) throws java.lang.IndexOutOfBoundsException + { + + // Check if the byte array is long enough to store the short + + if (bytarray.length < pos) + throw new java.lang.IndexOutOfBoundsException(); + + // Pack the short value + + bytarray[pos] = (byte) ((val >> 8) & 0xFF); + bytarray[pos + 1] = (byte) (val & 0xFF); + } + + /** + * Pack a string into a data buffer + * + * @param str String to be packed into the buffer + * @param bytarray Byte array to pack the string into + * @param pos Position to start packing the string + * @param nullterm true if the string should be null terminated, else false + * @return The ending buffer position + */ + public final static int putString(String str, byte[] bytarray, int pos, boolean nullterm) + { + + // Get the string as a byte array + + byte[] byts = str.getBytes(); + + // Pack the data bytes + + int bufpos = pos; + + for (int i = 0; i < byts.length; i++) + bytarray[bufpos++] = byts[i]; + + // Null terminate the string, if required + + if (nullterm == true) + bytarray[bufpos++] = 0; + + // Return the next free buffer position + + return bufpos; + } + + /** + * Pack a string into a data buffer + * + * @param str String to be packed into the buffer + * @param fldLen Field length, will be space padded if short + * @param bytarray Byte array to pack the string into + * @param pos Position to start packing the string + * @return The ending buffer position + */ + public final static int putString(String str, int fldLen, byte[] bytarray, int pos) + { + + // Get the string as a byte array + + byte[] byts = str.getBytes(); + + // Pack the data bytes + + int bufpos = pos; + int idx = 0; + + while (idx < fldLen) + { + if (idx < byts.length) + bytarray[bufpos++] = byts[idx]; + else + bytarray[bufpos++] = (byte) 0; + idx++; + } + + // Return the next free buffer position + + return bufpos; + } + + /** + * Pack a string into a data buffer. The string may be ASCII or Unicode. + * + * @param str String to be packed into the buffer + * @param bytarray Byte array to pack the string into + * @param pos Position to start packing the string + * @param nullterm true if the string should be null terminated, else false + * @param isUni true if the string should be packed as Unicode, false to pack as ASCII + * @return The ending buffer position + */ + public final static int putString(String str, byte[] bytarray, int pos, boolean nullterm, boolean isUni) + { + + // Pack the string + + int newpos = -1; + + if (isUni) + newpos = putUnicodeString(str, bytarray, pos, nullterm); + else + newpos = putString(str, bytarray, pos, nullterm); + + // Return the end of string buffer position + + return newpos; + } + + /** + * Pack a Unicode string into a data buffer + * + * @param str String to be packed into the buffer + * @param bytarray Byte array to pack the string into + * @param pos Position to start packing the string + * @param nullterm true if the string should be null terminated, else false + * @return The ending buffer position + */ + public final static int putUnicodeString(String str, byte[] bytarray, int pos, boolean nullterm) + { + + // Pack the data bytes + + int bufpos = pos; + + for (int i = 0; i < str.length(); i++) + { + + // Get the current character from the string + + char ch = str.charAt(i); + + // Pack the unicode character + + bytarray[bufpos++] = (byte) (ch & 0xFF); + bytarray[bufpos++] = (byte) ((ch & 0xFF00) >> 8); + } + + // Null terminate the string, if required + + if (nullterm == true) + { + bytarray[bufpos++] = 0; + bytarray[bufpos++] = 0; + } + + // Return the next free buffer position + + return bufpos; + } + + /** + * Pack nulls into the buffer. + * + * @param buf Buffer to pack data into. + * @param pos Position to start packing. + * @param cnt Number of nulls to pack. + * @exception java.lang.ArrayIndexOutOfBoundsException If the buffer does not have enough space. + */ + public final static void putZeros(byte[] buf, int pos, int cnt) throws java.lang.ArrayIndexOutOfBoundsException + { + + // Check if the buffer is big enough + + if (buf.length < (pos + cnt)) + throw new java.lang.ArrayIndexOutOfBoundsException(); + + // Pack the nulls + + for (int i = 0; i < cnt; i++) + buf[pos + i] = 0; + } + + /** + * Align a buffer offset on a word boundary + * + * @param pos int + * @return int + */ + public final static int wordAlign(int pos) + { + return (pos + 1) & 0xFFFFFFFE; + } + + /** + * Align a buffer offset on a longword boundary + * + * @param pos int + * @return int + */ + public final static int longwordAlign(int pos) + { + return (pos + 3) & 0xFFFFFFFC; + } + + /** + * Calculate the string length in bytes + * + * @param str String + * @param uni boolean + * @param nul boolean + * @return int + */ + public final static int getStringLength(String str, boolean uni, boolean nul) + { + + // Calculate the string length in bytes + + int len = str.length(); + if (nul) + len += 1; + if (uni) + len *= 2; + + return len; + } + + /** + * Calculate the new buffer position after the specified string and encoding (ASCII or Unicode) + * + * @param pos int + * @param str String + * @param uni boolean + * @param nul boolean + * @return int + */ + public final static int getBufferPosition(int pos, String str, boolean uni, boolean nul) + { + + // Calculate the new buffer position + + int len = str.length(); + if (nul) + len += 1; + if (uni) + len *= 2; + + return pos + len; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/util/HexDump.java b/source/java/org/alfresco/filesys/util/HexDump.java new file mode 100644 index 0000000000..be04c80b52 --- /dev/null +++ b/source/java/org/alfresco/filesys/util/HexDump.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +import java.io.PrintStream; + +/** + * Hex dump class. + */ +public final class HexDump +{ + + /** + * Hex dump a byte array + * + * @param byt Byte array to dump + * @param len Length of data to dump + * @param offset Offset to start data dump + */ + + public static final void Dump(byte[] byt, int len, int offset) + { + Dump(byt, len, offset, System.out); + } + + /** + * Hex dump a byte array + * + * @param byt Byte array to dump + * @param len Length of data to dump + * @param offset Offset to start data dump + * @param stream Output stream to dump the output to. + */ + + public static final void Dump(byte[] byt, int len, int offset, PrintStream stream) + { + + // Create buffers for the ASCII and Hex output + + StringBuffer ascBuf = new StringBuffer(); + StringBuffer hexBuf = new StringBuffer(); + + // Dump 16 byte blocks from the array until the length has been + // reached + + int dlen = 0; + int doff = offset; + String posStr = null; + + while (dlen < len) + { + + // Reset the ASCII/Hex buffers + + ascBuf.setLength(0); + hexBuf.setLength(0); + + posStr = generatePositionString(doff); + + // Dump a block of data, update the data offset + + doff = generateLine(byt, doff, ascBuf, hexBuf); + + // Output the current record + + stream.print(posStr); + stream.print(hexBuf.toString()); + stream.println(ascBuf.toString()); + + // Update the dump length + + dlen += 16; + } + } + + /** + * Generate a hex string for the specified string + * + * @param str String + * @return String + */ + public static final String hexString(String str) + { + if (str != null) + return hexString(str.getBytes()); + return ""; + } + + /** + * Generate a hex string for the specified string + * + * @param str String + * @param gap String + * @return String + */ + public static final String hexString(String str, String gap) + { + if (str != null) + return hexString(str.getBytes(), gap); + return ""; + } + + /** + * Generate a hex string for the specified bytes + * + * @param buf byte[] + * @return String + */ + public static final String hexString(byte[] buf) + { + return hexString(buf, buf.length, null); + } + + /** + * Generate a hex string for the specified bytes + * + * @param buf byte[] + * @param gap String + * @return String + */ + public static final String hexString(byte[] buf, String gap) + { + return hexString(buf, buf.length, gap); + } + + /** + * Generate a hex string for the specified bytes + * + * @param buf byte[] + * @param len int + * @param gap String + * @return String + */ + public static final String hexString(byte[] buf, int len, String gap) + { + + // Check if the buffer is valid + + if (buf == null) + return ""; + + // Create a string buffer for the hex string + + int buflen = buf.length * 2; + if (gap != null) + buflen += buf.length * gap.length(); + + StringBuffer hex = new StringBuffer(buflen); + + // Convert the bytes to hex-ASCII + + for (int i = 0; i < len; i++) + { + + // Get the current byte + + int curbyt = (int) (buf[i] & 0x00FF); + + // Output the hex string + + hex.append(Integer.toHexString((curbyt & 0xF0) >> 4)); + hex.append(Integer.toHexString(curbyt & 0x0F)); + + // Add the gap string, if specified + + if (gap != null && i < (len - 1)) + hex.append(gap); + } + + // Return the hex-ASCII string + + return hex.toString(); + } + + /** + * Generate a buffer position string + * + * @param off int + * @return String + */ + private static final String generatePositionString(int off) + { + + // Create a buffer position string + + StringBuffer posStr = new StringBuffer("" + off + " - "); + while (posStr.length() < 8) + posStr.insert(0, " "); + + // Return the string + + return posStr.toString(); + } + + /** + * Output a single line of the hex dump to a debug device + * + * @param byt Byte array to dump + * @param off Offset to start data dump + * @param ascBuf Buffer for ASCII output + * @param hexBuf Buffer for Hex output + * @return New offset value + */ + + private static final int generateLine(byte[] byt, int off, StringBuffer ascBuf, StringBuffer hexBuf) + { + + // Check if there is enough buffer space to dump 16 bytes + + int dumplen = byt.length - off; + if (dumplen > 16) + dumplen = 16; + + // Dump a 16 byte block of data + + for (int i = 0; i < dumplen; i++) + { + + // Get the current byte + + int curbyt = (int) (byt[off++] & 0x00FF); + + // Output the hex string + + hexBuf.append(Integer.toHexString((curbyt & 0xF0) >> 4)); + hexBuf.append(Integer.toHexString(curbyt & 0x0F)); + hexBuf.append(" "); + + // Output the character equivalent, if printable + + if (Character.isLetterOrDigit((char) curbyt) || Character.getType((char) curbyt) != Character.CONTROL) + ascBuf.append((char) curbyt); + else + ascBuf.append("."); + } + + // Output the hex dump line + + hexBuf.append(" - "); + + // Return the new data offset + + return off; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/util/IPAddress.java b/source/java/org/alfresco/filesys/util/IPAddress.java new file mode 100644 index 0000000000..e1f1d713cb --- /dev/null +++ b/source/java/org/alfresco/filesys/util/IPAddress.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +import java.net.InetAddress; +import java.util.StringTokenizer; + +/** + * TCP/IP Address Utility Class + */ +public class IPAddress +{ + + /** + * Check if the specified address is a valid numeric TCP/IP address + * + * @param ipaddr String + * @return boolean + */ + public final static boolean isNumericAddress(String ipaddr) + { + + // Check if the string is valid + + if (ipaddr == null || ipaddr.length() < 7 || ipaddr.length() > 15) + return false; + + // Check the address string, should be n.n.n.n format + + StringTokenizer token = new StringTokenizer(ipaddr, "."); + if (token.countTokens() != 4) + return false; + + while (token.hasMoreTokens()) + { + + // Get the current token and convert to an integer value + + String ipNum = token.nextToken(); + + try + { + int ipVal = Integer.valueOf(ipNum).intValue(); + if (ipVal < 0 || ipVal > 255) + return false; + } + catch (NumberFormatException ex) + { + return false; + } + } + + // Looks like a valid IP address + + return true; + } + + /** + * Check if the specified address is a valid numeric TCP/IP address and return as an integer + * value + * + * @param ipaddr String + * @return int + */ + public final static int parseNumericAddress(String ipaddr) + { + + // Check if the string is valid + + if (ipaddr == null || ipaddr.length() < 7 || ipaddr.length() > 15) + return 0; + + // Check the address string, should be n.n.n.n format + + StringTokenizer token = new StringTokenizer(ipaddr, "."); + if (token.countTokens() != 4) + return 0; + + int ipInt = 0; + + while (token.hasMoreTokens()) + { + + // Get the current token and convert to an integer value + + String ipNum = token.nextToken(); + + try + { + + // Validate the current address part + + int ipVal = Integer.valueOf(ipNum).intValue(); + if (ipVal < 0 || ipVal > 255) + return 0; + + // Add to the integer address + + ipInt = (ipInt << 8) + ipVal; + } + catch (NumberFormatException ex) + { + return 0; + } + } + + // Return the integer address + + return ipInt; + } + + /** + * Convert an IP address into an integer value + * + * @param ipaddr InetAddress + * @return int + */ + public final static int asInteger(InetAddress ipaddr) + { + + // Get the address as an array of bytes + + byte[] addrBytes = ipaddr.getAddress(); + + // Build an integer value from the bytes + + return DataPacker.getInt(addrBytes, 0); + } + + /** + * Check if the specified address is within the required subnet + * + * @param ipaddr String + * @param subnet String + * @param mask String + * @return boolean + */ + public final static boolean isInSubnet(String ipaddr, String subnet, String mask) + { + + // Convert the addresses to integer values + + int ipaddrInt = parseNumericAddress(ipaddr); + if (ipaddrInt == 0) + return false; + + int subnetInt = parseNumericAddress(subnet); + if (subnetInt == 0) + return false; + + int maskInt = parseNumericAddress(mask); + if (maskInt == 0) + return false; + + // Check if the address is part of the subnet + + if ((ipaddrInt & maskInt) == subnetInt) + return true; + return false; + } + + /** + * Convert a raw IP address array as a String + * + * @param ipaddr byte[] + * @return String + */ + public final static String asString(byte[] ipaddr) + { + + // Check if the address is valid + + if (ipaddr == null || ipaddr.length != 4) + return null; + + // Convert the raw IP address to a string + + StringBuffer str = new StringBuffer(); + + str.append((int) (ipaddr[0] & 0xFF)); + str.append("."); + str.append((int) (ipaddr[1] & 0xFF)); + str.append("."); + str.append((int) (ipaddr[2] & 0xFF)); + str.append("."); + str.append((int) (ipaddr[3] & 0xFF)); + + // Return the address string + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/util/MemorySize.java b/source/java/org/alfresco/filesys/util/MemorySize.java new file mode 100644 index 0000000000..30d6d03ef9 --- /dev/null +++ b/source/java/org/alfresco/filesys/util/MemorySize.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +/** + * Memory Size Class + *

    + * Convenience class to convert memory size value specified as 'nK' for kilobytes, 'nM' for + * megabytes and 'nG' for gigabytes, to an absolute value. + */ +public class MemorySize +{ + // Convertor constants + + public static final long KILOBYTE = 1024L; + public static final long MEGABYTE = 1024L * KILOBYTE; + public static final long GIGABYTE = 1024L * MEGABYTE; + public static final long TERABYTE = 1024L * GIGABYTE; + + /** + * Convert a memory size to an integer byte value. + * + * @param memSize String + * @return int + * @exception NumberFormatException + */ + public static final int getByteValueInt(String memSize) + { + return (int) (getByteValue(memSize) & 0xFFFFFFFFL); + } + + /** + * Convert a memory size to a byte value + * + * @param memSize String + * @return long + * @exception NumberFormatException + */ + public static final long getByteValue(String memSize) + { + + // Check if the string is valid + + if (memSize == null || memSize.length() == 0) + return -1L; + + // Check for a kilobyte value + + String sizeStr = memSize.toUpperCase(); + long mult = 1; + long val = 0; + + if (sizeStr.endsWith("K")) + { + + // Use the kilobyte multiplier + + mult = KILOBYTE; + val = getValue(sizeStr); + } + else if (sizeStr.endsWith("M")) + { + + // Use the megabyte nultiplier + + mult = MEGABYTE; + val = getValue(sizeStr); + } + else if (sizeStr.endsWith("G")) + { + + // Use the gigabyte multiplier + + mult = GIGABYTE; + val = getValue(sizeStr); + } + else if (sizeStr.endsWith("T")) + { + + // Use the terabyte multiplier + + mult = TERABYTE; + val = getValue(sizeStr); + } + else + { + + // Convert a numeric byte value + + val = Long.valueOf(sizeStr).longValue(); + } + + // Apply the multiplier + + return val * mult; + } + + /** + * Get the size value from a string and return the numeric value + * + * @param val String + * @return long + * @exception NumberFormatException + */ + private final static long getValue(String val) + { + + // Strip the trailing size indicator + + String sizStr = val.substring(0, val.length() - 1); + return Long.valueOf(sizStr).longValue(); + } + + /** + * Return a byte value as a kilobyte string + * + * @param val long + * @return String + */ + public final static String asKilobyteString(long val) + { + + // Calculate the kilobyte value + + long mbVal = val / KILOBYTE; + return "" + mbVal + "Kb"; + } + + /** + * Return a byte value as a megabyte string + * + * @param val long + * @return String + */ + public final static String asMegabyteString(long val) + { + + // Calculate the megabyte value + + long mbVal = val / MEGABYTE; + return "" + mbVal + "Mb"; + } + + /** + * Return a byte value as a gigabyte string + * + * @param val long + * @return String + */ + public final static String asGigabyteString(long val) + { + + // Calculate the gigabyte value + + long mbVal = val / GIGABYTE; + return "" + mbVal + "Gb"; + } + + /** + * Return a byte value as a terabyte string + * + * @param val long + * @return String + */ + public final static String asTerabyteString(long val) + { + + // Calculate the terabyte value + + long mbVal = val / TERABYTE; + return "" + mbVal + "Tb"; + } + + /** + * Return a byte value as a scaled string + * + * @param val long + * @return String + */ + public final static String asScaledString(long val) + { + + // Determine the scaling to apply + + String ret = null; + + if (val < (KILOBYTE * 2L)) + ret = Long.toString(val); + else if (val < (MEGABYTE * 2L)) + ret = asKilobyteString(val); + else if (val < (GIGABYTE * 2L)) + ret = asMegabyteString(val); + else if (val < (TERABYTE * 2L)) + ret = asGigabyteString(val); + else + ret = asTerabyteString(val); + + return ret; + } +} diff --git a/source/java/org/alfresco/filesys/util/StringList.java b/source/java/org/alfresco/filesys/util/StringList.java new file mode 100644 index 0000000000..73923c1843 --- /dev/null +++ b/source/java/org/alfresco/filesys/util/StringList.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +import java.util.Vector; + +/** + * String List Class + */ +public class StringList +{ + + // List of strings + + private Vector m_list; + + /** + * Default constructor + */ + public StringList() + { + m_list = new Vector(); + } + + /** + * Return the number of strings in the list + * + * @return int + */ + public final int numberOfStrings() + { + return m_list.size(); + } + + /** + * Add a string to the list + * + * @param str String + */ + public final void addString(String str) + { + m_list.add(str); + } + + /** + * Add a list of strings to this list + * + * @param list StringList + */ + public final void addStrings(StringList list) + { + if (list != null && list.numberOfStrings() > 0) + for (int i = 0; i < list.numberOfStrings(); m_list.add(list.getStringAt(i++))) + ; + } + + /** + * Return the string at the specified index + * + * @param idx int + * @return String + */ + public final String getStringAt(int idx) + { + if (idx < 0 || idx >= m_list.size()) + return null; + return (String) m_list.elementAt(idx); + } + + /** + * Check if the list contains the specified string + * + * @param str String + * @return boolean + */ + public final boolean containsString(String str) + { + return m_list.contains(str); + } + + /** + * Return the index of the specified string, or -1 if not in the list + * + * @param str String + * @return int + */ + public final int findString(String str) + { + return m_list.indexOf(str); + } + + /** + * Remove the specified string from the list + * + * @param str String + * @return boolean + */ + public final boolean removeString(String str) + { + return m_list.removeElement(str); + } + + /** + * Remove the string at the specified index within the list + * + * @param idx int + * @return String + */ + public final String removeStringAt(int idx) + { + if (idx < 0 || idx >= m_list.size()) + return null; + String ret = (String) m_list.elementAt(idx); + m_list.removeElementAt(idx); + return ret; + } + + /** + * Clear the strings from the list + */ + public final void remoteAllStrings() + { + m_list.removeAllElements(); + } + + /** + * Return the string list as a string + * + * @return String + */ + public String toString() + { + + // Check if the list is empty + + if (numberOfStrings() == 0) + return ""; + + // Build the string + + StringBuffer str = new StringBuffer(); + + for (int i = 0; i < m_list.size(); i++) + { + str.append(getStringAt(i)); + str.append(","); + } + + // Remove the trailing comma + + if (str.length() > 0) + str.setLength(str.length() - 1); + + // Return the string + + return str.toString(); + } +} diff --git a/source/java/org/alfresco/filesys/util/WildCard.java b/source/java/org/alfresco/filesys/util/WildCard.java new file mode 100644 index 0000000000..63dc069e30 --- /dev/null +++ b/source/java/org/alfresco/filesys/util/WildCard.java @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.filesys.util; + +/** + * Wildcard Utility Class. + *

    + * The WildCard class may be used to check Strings against a wildcard pattern using the SMB/CIFS + * wildcard rules. + *

    + * A number of static convenience methods are also provided. + * + * @author GKSpencer + */ +public final class WildCard +{ + + // Multiple character wildcard + + public static final int MULTICHAR_WILDCARD = '*'; + + // Single character wildcard + + public static final int SINGLECHAR_WILDCARD = '?'; + + // Unicode wildcards + // + // The spec states :- + // translate '?' to '>' + // translate '.' to '"' if followed by a '?' or '*' + // translate '*' to '<' if followed by a '.' + + public static final int SINGLECHAR_UNICODE_WILDCARD = '>'; + public static final int DOT_UNICODE_WILDCARD = '"'; + public static final int MULTICHAR_UNICODE_WILDCARD = '<'; + + // Wildcard types + + public static final int WILDCARD_NONE = 0; // no wildcard characters present in pattern + public static final int WILDCARD_ALL = 1; // '*.*' and '*' + public static final int WILDCARD_NAME = 2; // '*.ext' + public static final int WILDCARD_EXT = 3; // 'name.*' + public static final int WILDCARD_COMPLEX = 4; // complex wildcard + + public static final int WILDCARD_INVALID = -1; + + // Wildcard pattern and type + + private String m_pattern; + private int m_type; + + // Start/end string to match for name/extension matching + + private String m_matchPart; + private boolean m_caseSensitive; + + // Complex wildcard pattern + + private char[] m_patternChars; + + /** + * Default constructor + */ + public WildCard() + { + setType(WILDCARD_INVALID); + } + + /** + * Class constructor + * + * @param pattern String + * @param caseSensitive boolean + */ + public WildCard(String pattern, boolean caseSensitive) + { + setPattern(pattern, caseSensitive); + } + + /** + * Return the wildcard pattern type + * + * @return int + */ + public final int isType() + { + return m_type; + } + + /** + * Check if case sensitive matching is enabled + * + * @return boolean + */ + public final boolean isCaseSensitive() + { + return m_caseSensitive; + } + + /** + * Return the wildcard pattern string + * + * @return String + */ + public final String getPattern() + { + return m_pattern; + } + + /** + * Return the match part for wildcard name and wildcard extension type patterns + * + * @return String + */ + public final String getMatchPart() + { + return m_matchPart; + } + + /** + * Determine if the string matches the wildcard pattern + * + * @param str String + * @return boolean + */ + public final boolean matchesPattern(String str) + { + + // Check the pattern type and compare the string + + boolean sts = false; + + switch (isType()) + { + + // Match all wildcard + + case WILDCARD_ALL: + sts = true; + break; + + // Match any name + + case WILDCARD_NAME: + if (isCaseSensitive()) + { + + // Check if the string ends with the required file extension + + sts = str.endsWith(m_matchPart); + } + else + { + + // Normalize the string and compare + + String upStr = str.toUpperCase(); + sts = upStr.endsWith(m_matchPart); + } + break; + + // Match any file extension + + case WILDCARD_EXT: + if (isCaseSensitive()) + { + + // Check if the string starts with the required file name + + sts = str.startsWith(m_matchPart); + } + else + { + + // Normalize the string and compare + + String upStr = str.toUpperCase(); + sts = upStr.startsWith(m_matchPart); + } + break; + + // Complex wildcard matching + + case WILDCARD_COMPLEX: + if (isCaseSensitive()) + sts = matchComplexWildcard(str); + else + { + + // Normalize the string and compare + + String upStr = str.toUpperCase(); + sts = matchComplexWildcard(upStr); + } + break; + + // No wildcard characters in pattern, compare strings + + case WILDCARD_NONE: + if (isCaseSensitive()) + { + if (str.compareTo(m_pattern) == 0) + sts = true; + } + else if (str.equalsIgnoreCase(m_pattern)) + sts = true; + break; + } + + // Return the wildcard match status + + return sts; + } + + /** + * Match a complex wildcard pattern with the specified string + * + * @param str String + * @return boolean + */ + protected final boolean matchComplexWildcard(String str) + { + + // Convert the string to a char array for matching + + char[] strChars = str.toCharArray(); + + // Compare the string to the wildcard pattern + + int wpos = 0; + int wlen = m_patternChars.length; + + int spos = 0; + int slen = strChars.length; + + char patChar; + boolean matchFailed = false; + + while (matchFailed == false && wpos < m_patternChars.length) + { + + // Match the current pattern character + + patChar = m_patternChars[wpos++]; + + switch (patChar) + { + + // Match single character + + case SINGLECHAR_WILDCARD: + if (spos < slen) + spos++; + else + matchFailed = true; + break; + + // Match zero or more characters + + case MULTICHAR_WILDCARD: + + // Check if there is another character in the wildcard pattern + + if (wpos < wlen) + { + + // Check if the character is not a wildcard character + + patChar = m_patternChars[wpos]; + if (patChar != SINGLECHAR_WILDCARD && patChar != MULTICHAR_WILDCARD) + { + + // Find the required character in the string + + while (spos < slen && strChars[spos] != patChar) + spos++; + if (spos >= slen) + matchFailed = true; + } + } + else + { + + // Multi character wildcard at the end of the pattern, match all remaining + // characters + + spos = slen; + } + break; + + // Match the pattern and string character + + default: + if (spos >= slen || strChars[spos] != patChar) + matchFailed = true; + else + spos++; + break; + } + } + + // Check if the match was successul and return status + + if (matchFailed == false && spos == slen) + return true; + return false; + } + + /** + * Set the wildcard pattern string + * + * @param pattern String + * @param caseSensitive boolean + */ + public final void setPattern(String pattern, boolean caseSensitive) + { + + // Save the pattern string and case sensitive flag + + m_pattern = pattern; + m_caseSensitive = caseSensitive; + + setType(WILDCARD_INVALID); + + // Check if the pattern string is valid + + if (pattern == null || pattern.length() == 0) + return; + + // Check for the match all wildcard + + if (pattern.compareTo("*.*") == 0 || pattern.compareTo("*") == 0) + { + setType(WILDCARD_ALL); + return; + } + + // Check for a name wildcard, ie. '*.ext' + + if (pattern.startsWith("*.")) + { + + // Split the string to get the extension string + + if (pattern.length() > 2) + m_matchPart = pattern.substring(1); + else + m_matchPart = ""; + + // If matching is case insensitive then normalize the string + + if (isCaseSensitive() == false) + m_matchPart = m_matchPart.toUpperCase(); + + // If the file extension contains wildcards we will need to use a regular expression + + if (containsWildcards(m_matchPart) == false) + { + setType(WILDCARD_NAME); + return; + } + } + + // Check for a file extension wildcard + + if (pattern.endsWith(".*")) + { + + // Split the string to get the name string + + if (pattern.length() > 2) + m_matchPart = pattern.substring(0, pattern.length() - 2); + else + m_matchPart = ""; + + // If matching is case insensitive then normalize the string + + if (isCaseSensitive() == false) + m_matchPart = m_matchPart.toUpperCase(); + + // If the file name contains wildcards we will need to use a regular expression + + if (containsWildcards(m_matchPart) == false) + { + setType(WILDCARD_EXT); + return; + } + } + + // Save the complex wildcard pattern as a char array for later pattern matching + + if (isCaseSensitive() == false) + m_patternChars = m_pattern.toUpperCase().toCharArray(); + else + m_patternChars = m_pattern.toCharArray(); + + setType(WILDCARD_COMPLEX); + } + + /** + * Set the wildcard type + * + * @param typ int + */ + private final void setType(int typ) + { + m_type = typ; + } + + /** + * Return the wildcard as a string + * + * @return String + */ + public String toString() + { + StringBuffer str = new StringBuffer(); + str.append("["); + str.append(getPattern()); + str.append(","); + str.append(isType()); + str.append(","); + + if (m_matchPart != null) + str.append(m_matchPart); + + if (isCaseSensitive()) + str.append(",Case"); + else + str.append(",NoCase"); + str.append("]"); + + return str.toString(); + } + + /** + * Check if the string contains any wildcard characters. + * + * @return boolean + * @param str java.lang.String + */ + public final static boolean containsWildcards(String str) + { + + // Check the string for wildcard characters + + if (str.indexOf(MULTICHAR_WILDCARD) != -1) + return true; + + if (str.indexOf(SINGLECHAR_WILDCARD) != -1) + return true; + + // No wildcards found in the string + + return false; + } + + /** + * Check if a string contains any of the Unicode wildcard characters + * + * @param str String + * @return boolean + */ + public final static boolean containsUnicodeWildcard(String str) + { + + // Check if the string contains any of the Unicode wildcards + + if (str.indexOf(SINGLECHAR_UNICODE_WILDCARD) != -1 || str.indexOf(MULTICHAR_UNICODE_WILDCARD) != -1 + || str.indexOf(DOT_UNICODE_WILDCARD) != -1) + return true; + return false; + } + + /** + * Convert the Unicode wildcard string to a standard DOS wildcard string + * + * @param str String + * @return String + */ + public final static String convertUnicodeWildcardToDOS(String str) + { + + // Create a buffer for the new wildcard string + + StringBuffer newStr = new StringBuffer(str.length()); + + // Convert the Unicode wildcard string to a DOS wildcard string + + for (int i = 0; i < str.length(); i++) + { + + // Get the current character + + char ch = str.charAt(i); + + // Check for a Unicode wildcard character + + if (ch == SINGLECHAR_UNICODE_WILDCARD) + { + + // Translate to the DOS single character wildcard character + + ch = SINGLECHAR_WILDCARD; + } + else if (ch == MULTICHAR_UNICODE_WILDCARD) + { + + // Check if the current character is followed by a '.', if so then translate to the + // DOS multi character + // wildcard + + if (i < (str.length() - 1) && str.charAt(i + 1) == '.') + ch = MULTICHAR_WILDCARD; + } + else if (ch == DOT_UNICODE_WILDCARD) + { + + // Check if the current character is followed by a DOS single/multi character + // wildcard + + if (i < (str.length() - 1)) + { + char nextCh = str.charAt(i + 1); + if (nextCh == SINGLECHAR_WILDCARD || nextCh == MULTICHAR_WILDCARD + || nextCh == SINGLECHAR_UNICODE_WILDCARD) + ch = '.'; + } + } + + // Append the character to the translated wildcard string + + newStr.append(ch); + } + + // Return the translated wildcard string + + return newStr.toString(); + } + + /** + * Convert a wildcard string to a regular expression + * + * @param path String + * @return String + */ + public final static String convertToRegexp(String path) + { + + // Convert the path to characters, check if the wildcard string ends with a single character + // wildcard + + char[] smbPattern = path.toCharArray(); + boolean endsWithQ = smbPattern[smbPattern.length - 1] == '?'; + + // Build up the regular expression + + StringBuffer sb = new StringBuffer(); + sb.append('^'); + + for (int i = 0; i < smbPattern.length; i++) + { + + // Process the current character + + switch (smbPattern[i]) + { + + // Multi character wildcard + + case '*': + sb.append(".*"); + break; + + // Single character wildcard + + case '?': + if (endsWithQ) + { + boolean restQ = true; + for (int j = i + 1; j < smbPattern.length; j++) + { + if (smbPattern[j] != '?') + { + restQ = false; + break; + } + } + if (restQ) + sb.append(".?"); + else + sb.append('.'); + } + else + sb.append('.'); + break; + + // Escape regular expression special characters + + case '.': + case '+': + case '\\': + case '[': + case ']': + case '^': + case '$': + case '(': + case ')': + sb.append('\\'); + sb.append(smbPattern[i]); + break; + + // Normal characters, just pass through + + default: + sb.append(smbPattern[i]); + break; + } + } + sb.append('$'); + + // Return the regular expression string + + return sb.toString(); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/model/ContentModel.java b/source/java/org/alfresco/model/ContentModel.java new file mode 100644 index 0000000000..d73abb1618 --- /dev/null +++ b/source/java/org/alfresco/model/ContentModel.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.model; + +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + + +/** + * Content Model Constants + */ +public interface ContentModel +{ + // + // System Model Definitions + // + + // base type constants + static final QName TYPE_BASE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "base"); + static final QName ASPECT_REFERENCEABLE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "referenceable"); + static final QName PROP_STORE_PROTOCOL = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "store-protocol"); + static final QName PROP_STORE_IDENTIFIER = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "store-identifier"); + static final QName PROP_NODE_UUID = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "node-uuid"); + + // referenceable aspect constants + static final QName TYPE_REFERENCE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "reference"); + static final QName PROP_REFERENCE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "reference"); + + // container type constants + static final QName TYPE_CONTAINER = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "container"); + /** child association type supported by {@link #TYPE_CONTAINER} */ + static final QName ASSOC_CHILDREN =QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "children"); + + // roots + static final QName ASPECT_ROOT = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "aspect_root"); + static final QName TYPE_STOREROOT = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "store_root"); + + + // + // Content Model Definitions + // + + // content management type constants + static final QName TYPE_CMOBJECT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "cmobject"); + static final QName PROP_NAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "name"); + + // copy aspect constants + static final QName ASPECT_COPIEDFROM = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "copiedfrom"); + static final QName PROP_COPY_REFERENCE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "source"); + + // working copy aspect contants + static final QName ASPECT_WORKING_COPY = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "workingcopy"); + static final QName PROP_WORKING_COPY_OWNER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "workingCopyOwner"); + + // content type and aspect constants + static final QName TYPE_CONTENT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "content"); + static final QName PROP_CONTENT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "content"); + + // title aspect + static final QName ASPECT_TITLED = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "titled"); + static final QName PROP_TITLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "title"); + static final QName PROP_DESCRIPTION = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "description"); + + // auditable aspect + static final QName ASPECT_AUDITABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "auditable"); + static final QName PROP_CREATED = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "created"); + static final QName PROP_CREATOR = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "creator"); + static final QName PROP_MODIFIED = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modified"); + static final QName PROP_MODIFIER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modifier"); + static final QName PROP_ACCESSED = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "accessed"); + + // categories + static final QName TYPE_CATEGORYROOT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "category_root"); + static final QName ASPECT_CLASSIFIABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "classifiable"); + //static final QName ASPECT_CATEGORISATION = QName.createQName(NamespaceService.ALFRESCO_URI, "aspect_categorisation"); + static final QName ASPECT_GEN_CLASSIFIABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "generalclassifiable"); + static final QName TYPE_CATEGORY = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "category"); + static final QName PROP_CATEGORIES = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categories"); + static final QName ASSOC_CATEGORIES = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categories"); + static final QName ASSOC_SUBCATEGORIES = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "subcategories"); + + // lock aspect + public final static QName ASPECT_LOCKABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "lockable"); + public final static QName PROP_LOCK_OWNER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "lockOwner"); + public final static QName PROP_LOCK_TYPE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "lockType"); + public final static QName PROP_EXPIRY_DATE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "expiryDate"); + + // version aspect + static final QName ASPECT_VERSIONABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "versionable"); + static final QName PROP_VERSION_LABEL = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "versionLabel"); + static final QName PROP_AUTO_VERSION = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "autoVersion"); + + // folders + static final QName TYPE_SYSTEM_FOLDER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "systemfolder"); + static final QName TYPE_FOLDER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "folder"); + /** child association type supported by {@link #TYPE_FOLDER} */ + static final QName ASSOC_CONTAINS = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "contains"); + + // person + static final QName TYPE_PERSON = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "person"); + static final QName PROP_USERNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "userName"); + static final QName PROP_HOMEFOLDER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "homeFolder"); + static final QName PROP_FIRSTNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "firstName"); + static final QName PROP_LASTNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "lastName"); + static final QName PROP_EMAIL = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "email"); + static final QName PROP_ORGID = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "organizationId"); + + // Ownable aspect + static final QName ASPECT_OWNABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "ownable"); + static final QName PROP_OWNER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "owner"); + + // Templatable aspect + static final QName ASPECT_TEMPLATABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "templatable"); + static final QName PROP_TEMPLATE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "template"); + + // Dictionary model content type + + public static final QName TYPE_DICTIONARY_MODEL = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "dictionaryModel"); + + public static final QName PROP_MODEL_NAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modelName"); + public static final QName PROP_MODEL_DESCRIPTION = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modelDescription"); + public static final QName PROP_MODEL_AUTHOR = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modelAuthor"); + public static final QName PROP_MODEL_PUBLISHED_DATE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modelPublishedDate"); + public static final QName PROP_MODEL_VERSION = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modelVersion"); + public static final QName PROP_MODEL_ACTIVE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "modelActive"); + + // referencing aspect + public static final QName ASPECT_REFERENCING = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "referencing"); + public static final QName ASSOC_REFERENCES = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "references"); + + // + // Application Model Definitions + // + + // workflow + static final QName ASPECT_SIMPLE_WORKFLOW = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "simpleworkflow"); + static final QName PROP_APPROVE_STEP = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveStep"); + static final QName PROP_APPROVE_FOLDER = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveFolder"); + static final QName PROP_APPROVE_MOVE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveMove"); + static final QName PROP_REJECT_STEP = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectStep"); + static final QName PROP_REJECT_FOLDER = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectFolder"); + static final QName PROP_REJECT_MOVE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectMove"); + + // ui facets aspect + static final QName ASPECT_UIFACETS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "uifacets"); + static final QName PROP_ICON = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "icon"); + + // inlineeditable aspect + static final QName ASPECT_INLINEEDITABLE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "inlineeditable"); + static final QName PROP_EDITINLINE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "editInline"); + + // configurable + static final QName ASPECT_CONFIGURABLE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurable"); + static final QName TYPE_CONFIGURATIONS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurations"); + static final QName ASSOC_CONFIGURATIONS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurations"); + + + // + // User Model Definitions + // + + static final String USER_MODEL_URI = "http://www.alfresco.org/model/user/1.0"; + static final String USER_MODEL_PREFIX = "usr"; + + static final QName TYPE_USER = QName.createQName(USER_MODEL_URI, "user"); + static final QName PROP_USER_USERNAME = QName.createQName(USER_MODEL_URI, "username"); + static final QName PROP_PASSWORD = QName.createQName(USER_MODEL_URI, "password"); + static final QName PROP_ENABLED = QName.createQName(USER_MODEL_URI, "enabled"); + static final QName PROP_ACCOUNT_EXPIRES = QName.createQName(USER_MODEL_URI, "accountExpires"); + static final QName PROP_ACCOUNT_EXPIRY_DATE = QName.createQName(USER_MODEL_URI, "accountExpiryDate"); + static final QName PROP_CREDENTIALS_EXPIRE = QName.createQName(USER_MODEL_URI, "credentialsExpire"); + static final QName PROP_CREDENTIALS_EXPIRY_DATE = QName.createQName(USER_MODEL_URI, "credentialsExpiryDate"); + static final QName PROP_ACCOUNT_LOCKED = QName.createQName(USER_MODEL_URI, "accountLocked"); + static final QName PROP_SALT = QName.createQName(USER_MODEL_URI, "salt"); + + static final QName TYPE_AUTHORITY = QName.createQName(USER_MODEL_URI, "authority"); + + static final QName TYPE_AUTHORITY_CONTAINER = QName.createQName(USER_MODEL_URI, "authorityContainer"); + static final QName PROP_AUTHORITY_NAME = QName.createQName(USER_MODEL_URI, "authorityName"); + static final QName ASSOC_MEMBER = QName.createQName(USER_MODEL_URI, "member"); + static final QName PROP_MEMBERS = QName.createQName(USER_MODEL_URI, "members"); + + +} diff --git a/source/java/org/alfresco/model/ForumModel.java b/source/java/org/alfresco/model/ForumModel.java new file mode 100644 index 0000000000..0c0b838a51 --- /dev/null +++ b/source/java/org/alfresco/model/ForumModel.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.model; + +import org.alfresco.service.namespace.QName; + + +/** + * Forums Model Constants + */ +public interface ForumModel +{ + // + // Forums Model Definitions + // + + static final String FORUMS_MODEL_URI = "http://www.alfresco.org/model/forum/1.0"; + static final String FORUMS_MODEL_PREFIX = "fm"; + + static final QName TYPE_FORUMS = QName.createQName(FORUMS_MODEL_URI, "forums"); + static final QName TYPE_FORUM = QName.createQName(FORUMS_MODEL_URI, "forum"); + static final QName TYPE_TOPIC = QName.createQName(FORUMS_MODEL_URI, "topic"); + static final QName TYPE_POST = QName.createQName(FORUMS_MODEL_URI, "post"); + + static final QName ASPECT_DISCUSSABLE = QName.createQName(FORUMS_MODEL_URI, "discussable"); + + static final QName PROP_STATUS = QName.createQName(FORUMS_MODEL_URI, "status"); + static final QName PROP_TYPE = QName.createQName(FORUMS_MODEL_URI, "type"); +} diff --git a/source/java/org/alfresco/repo/action/ActionConditionDefinitionImpl.java b/source/java/org/alfresco/repo/action/ActionConditionDefinitionImpl.java new file mode 100644 index 0000000000..c9e4d65ee2 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionConditionDefinitionImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.alfresco.service.cmr.action.ActionConditionDefinition; + +/** + * Rule condition implementation class. + * + * @author Roy Wetherall + */ +public class ActionConditionDefinitionImpl extends ParameterizedItemDefinitionImpl + implements ActionConditionDefinition +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3688505493618177331L; + + /** + * ActionCondition evaluator + */ + private String conditionEvaluator; + + /** + * Constructor + * + * @param name the name + */ + public ActionConditionDefinitionImpl(String name) + { + super(name); + } + + /** + * Set the condition evaluator + * + * @param conditionEvaluator the condition evaluator + */ + public void setConditionEvaluator(String conditionEvaluator) + { + this.conditionEvaluator = conditionEvaluator; + } + + /** + * Get the condition evaluator + * + * @return the condition evaluator + */ + public String getConditionEvaluator() + { + return conditionEvaluator; + } +} diff --git a/source/java/org/alfresco/repo/action/ActionConditionDefinitionImplTest.java b/source/java/org/alfresco/repo/action/ActionConditionDefinitionImplTest.java new file mode 100644 index 0000000000..6c8f0d5a74 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionConditionDefinitionImplTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.alfresco.service.cmr.rule.RuleServiceException; + + +/** + * @author Roy Wetherall + */ +public class ActionConditionDefinitionImplTest extends BaseParameterizedItemDefinitionImplTest +{ + /** + * Constants used during tests + */ + private static final String CONDITION_EVALUATOR = "conditionEvaluator"; + + /** + * @see org.alfresco.repo.rule.common.RuleItemDefinitionImplTest#create() + */ + protected ParameterizedItemDefinitionImpl create() + { + // Test duplicate param name + try + { + ActionConditionDefinitionImpl temp = new ActionConditionDefinitionImpl(NAME); + temp.setParameterDefinitions(this.duplicateParamDefs); + fail("Duplicate param names are not allowed."); + } + catch (RuleServiceException exception) + { + // Indicates that there are duplicate param names + } + + // Create a good one + ActionConditionDefinitionImpl temp = new ActionConditionDefinitionImpl(NAME); + assertNotNull(temp); + //temp.setTitle(TITLE); + //temp.setDescription(DESCRIPTION); + temp.setParameterDefinitions(this.paramDefs); + temp.setConditionEvaluator(CONDITION_EVALUATOR); + return temp; + } + + /** + * Test getConditionEvaluator + */ + public void testGetConditionEvaluator() + { + ActionConditionDefinitionImpl cond = (ActionConditionDefinitionImpl)create(); + assertEquals(CONDITION_EVALUATOR, cond.getConditionEvaluator()); + } +} diff --git a/source/java/org/alfresco/repo/action/ActionConditionImpl.java b/source/java/org/alfresco/repo/action/ActionConditionImpl.java new file mode 100644 index 0000000000..b5b7dc05d6 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionConditionImpl.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.service.cmr.action.ActionCondition; + +/** + * @author Roy Wetherall + */ +public class ActionConditionImpl extends ParameterizedItemImpl implements Serializable, + ActionCondition +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3257288015402644020L; + + /** + * Rule condition defintion + */ + private String actionConditionDefinitionName; + + /** + * Indicates whether the result of the condition should have the NOT logical operator applied + * to it. + */ + private boolean invertCondition = false; + + /** + * Constructor + */ + public ActionConditionImpl(String id, String actionConditionDefinitionName) + { + this(id, actionConditionDefinitionName, null); + } + + /** + * @param parameterValues + */ + public ActionConditionImpl( + String id, + String actionConditionDefinitionName, + Map parameterValues) + { + super(id, parameterValues); + this.actionConditionDefinitionName = actionConditionDefinitionName; + } + + /** + * @see org.alfresco.service.cmr.action.ActionCondition#getActionConditionDefinitionName() + */ + public String getActionConditionDefinitionName() + { + return this.actionConditionDefinitionName; + } + + /** + * @see org.alfresco.service.cmr.action.ActionCondition#setInvertCondition(boolean) + */ + public void setInvertCondition(boolean invertCondition) + { + this.invertCondition = invertCondition; + } + + /** + * @see org.alfresco.service.cmr.action.ActionCondition#getInvertCondition() + */ + public boolean getInvertCondition() + { + return this.invertCondition; + } +} diff --git a/source/java/org/alfresco/repo/action/ActionConditionImplTest.java b/source/java/org/alfresco/repo/action/ActionConditionImplTest.java new file mode 100644 index 0000000000..10bd2b21d5 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionConditionImplTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.alfresco.service.cmr.action.ActionCondition; + +/** + * @author Roy Wetherall + */ +public class ActionConditionImplTest extends BaseParameterizedItemImplTest +{ + /** + * @see org.alfresco.repo.rule.common.RuleItemImplTest#create() + */ + @Override + protected ParameterizedItemImpl create() + { + return new ActionConditionImpl( + ID, + NAME, + this.paramValues); + } + + public void testGetRuleConditionDefintion() + { + ActionCondition temp = (ActionCondition)create(); + assertEquals(NAME, temp.getActionConditionDefinitionName()); + } + + public void testSetGetInvertCondition() + { + ActionCondition temp = (ActionCondition)create(); + assertFalse(temp.getInvertCondition()); + temp.setInvertCondition(true); + assertTrue(temp.getInvertCondition()); + } +} diff --git a/source/java/org/alfresco/repo/action/ActionDefinitionImpl.java b/source/java/org/alfresco/repo/action/ActionDefinitionImpl.java new file mode 100644 index 0000000000..2eaa034498 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionDefinitionImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.alfresco.service.cmr.action.ActionDefinition; + +/** + * Rule action implementation class + * + * @author Roy Wetherall + */ +public class ActionDefinitionImpl extends ParameterizedItemDefinitionImpl + implements ActionDefinition +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 4048797883396863026L; + + /** + * The rule action executor + */ + private String ruleActionExecutor; + + /** + * Constructor + * + * @param name the name + */ + public ActionDefinitionImpl(String name) + { + super(name); + } + + /** + * Set the rule action executor + * + * @param ruleActionExecutor the rule action executor + */ + public void setRuleActionExecutor(String ruleActionExecutor) + { + this.ruleActionExecutor = ruleActionExecutor; + } + + /** + * Get the rule aciton executor + * + * @return the rule action executor + */ + public String getRuleActionExecutor() + { + return ruleActionExecutor; + } +} diff --git a/source/java/org/alfresco/repo/action/ActionDefinitionImplTest.java b/source/java/org/alfresco/repo/action/ActionDefinitionImplTest.java new file mode 100644 index 0000000000..59240ec200 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionDefinitionImplTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.alfresco.service.cmr.rule.RuleServiceException; + + +/** + * @author Roy Wetherall + */ +public class ActionDefinitionImplTest extends BaseParameterizedItemDefinitionImplTest +{ + private static final String RULE_ACTION_EXECUTOR = "ruleActionExector"; + + protected ParameterizedItemDefinitionImpl create() + { + // Test duplicate param name + try + { + ActionDefinitionImpl temp = new ActionDefinitionImpl(NAME); + temp.setParameterDefinitions(duplicateParamDefs); + fail("Duplicate param names are not allowed."); + } + catch (RuleServiceException exception) + { + // Indicates that there are duplicate param names + } + + // Create a good one + ActionDefinitionImpl temp = new ActionDefinitionImpl(NAME); + assertNotNull(temp); + //temp.setTitle(TITLE); + // temp.setDescription(DESCRIPTION); + temp.setParameterDefinitions(paramDefs); + temp.setRuleActionExecutor(RULE_ACTION_EXECUTOR); + return temp; + } + + /** + * Test getRuleActionExecutor + */ + public void testGetRuleActionExecutor() + { + ActionDefinitionImpl temp = (ActionDefinitionImpl)create(); + assertEquals(RULE_ACTION_EXECUTOR, temp.getRuleActionExecutor()); + } +} diff --git a/source/java/org/alfresco/repo/action/ActionImpl.java b/source/java/org/alfresco/repo/action/ActionImpl.java new file mode 100644 index 0000000000..585e277bba --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionImpl.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Action implementation + * + * @author Roy Wetherall + */ +public class ActionImpl extends ParameterizedItemImpl + implements Serializable, Action +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3258135760426186548L; + + /** + * The title + */ + private String title; + + /** + * The description + */ + private String description; + + /** + * Inidcates whether the action should be executed asynchronously or not + */ + private boolean executeAsynchronously = false; + + /** + * The compensating action + */ + private Action compensatingAction; + + /** + * The created date + */ + private Date createdDate; + + /** + * The creator + */ + private String creator; + + /** + * The modified date + */ + private Date modifiedDate; + + /** + * The modifier + */ + private String modifier; + + /** + * Rule action definition name + */ + private String actionDefinitionName; + + /** + * The owning node reference + */ + private NodeRef owningNodeRef; + + /** + * The chain of actions that have lead to this action + */ + private Set actionChain; + + /** + * Action conditions + */ + private List actionConditions = new ArrayList(); + + /** + * Constructor + * + * @param id the action id + * @param actionDefinitionName the name of the action definition + */ + public ActionImpl(String id, String actionDefinitionName, NodeRef owningNodeRef) + { + this(id, actionDefinitionName, owningNodeRef, null); + } + + /** + * Constructor + * + * @param id the action id + * @param actionDefinitionName the action definition name + * @param parameterValues the parameter values + */ + public ActionImpl( + String id, + String actionDefinitionName, + NodeRef owningNodeRef, + Map parameterValues) + { + super(id, parameterValues); + this.actionDefinitionName = actionDefinitionName; + this.owningNodeRef = owningNodeRef; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getTitle() + */ + public String getTitle() + { + return this.title; + } + + /** + * @see org.alfresco.service.cmr.action.Action#setTitle(java.lang.String) + */ + public void setTitle(String title) + { + this.title = title; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getDescription() + */ + public String getDescription() + { + return this.description; + } + + /** + * @see org.alfresco.service.cmr.action.Action#setDescription(java.lang.String) + */ + public void setDescription(String description) + { + this.description = description; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getOwningNodeRef() + */ + public NodeRef getOwningNodeRef() + { + return this.owningNodeRef; + } + + public void setOwningNodeRef(NodeRef owningNodeRef) + { + this.owningNodeRef = owningNodeRef; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getExecuteAsychronously() + */ + public boolean getExecuteAsychronously() + { + return this.executeAsynchronously ; + } + + /** + * @see org.alfresco.service.cmr.action.Action#setExecuteAsynchronously(boolean) + */ + public void setExecuteAsynchronously(boolean executeAsynchronously) + { + this.executeAsynchronously = executeAsynchronously; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getCompensatingAction() + */ + public Action getCompensatingAction() + { + return this.compensatingAction; + } + + /** + * @see org.alfresco.service.cmr.action.Action#setCompensatingAction(org.alfresco.service.cmr.action.Action) + */ + public void setCompensatingAction(Action action) + { + this.compensatingAction = action; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getCreatedDate() + */ + public Date getCreatedDate() + { + return this.createdDate; + } + + /** + * Set the created date + * + * @param createdDate the created date + */ + public void setCreatedDate(Date createdDate) + { + this.createdDate = createdDate; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getCreator() + */ + public String getCreator() + { + return this.creator; + } + + /** + * Set the creator + * + * @param creator the creator + */ + public void setCreator(String creator) + { + this.creator = creator; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getModifiedDate() + */ + public Date getModifiedDate() + { + return this.modifiedDate; + } + + /** + * Set the modified date + * + * @param modifiedDate the modified date + */ + public void setModifiedDate(Date modifiedDate) + { + this.modifiedDate = modifiedDate; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getModifier() + */ + public String getModifier() + { + return this.modifier; + } + + /** + * Set the modifier + * + * @param modifier the modifier + */ + public void setModifier(String modifier) + { + this.modifier = modifier; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getActionDefinitionName() + */ + public String getActionDefinitionName() + { + return this.actionDefinitionName; + } + + /** + * @see org.alfresco.service.cmr.action.Action#hasActionConditions() + */ + public boolean hasActionConditions() + { + return (this.actionConditions.isEmpty() == false); + } + + /** + * @see org.alfresco.service.cmr.action.Action#indexOfActionCondition(org.alfresco.service.cmr.action.ActionCondition) + */ + public int indexOfActionCondition(ActionCondition actionCondition) + { + return this.actionConditions.indexOf(actionCondition); + } + + /** + * @see org.alfresco.service.cmr.action.Action#getActionConditions() + */ + public List getActionConditions() + { + return this.actionConditions; + } + + /** + * @see org.alfresco.service.cmr.action.Action#getActionCondition(int) + */ + public ActionCondition getActionCondition(int index) + { + return this.actionConditions.get(index); + } + + /** + * @see org.alfresco.service.cmr.action.Action#addActionCondition(org.alfresco.service.cmr.action.ActionCondition) + */ + public void addActionCondition(ActionCondition actionCondition) + { + this.actionConditions.add(actionCondition); + } + + /** + * @see org.alfresco.service.cmr.action.Action#addActionCondition(int, org.alfresco.service.cmr.action.ActionCondition) + */ + public void addActionCondition(int index, ActionCondition actionCondition) + { + this.actionConditions.add(index, actionCondition); + } + + /** + * @see org.alfresco.service.cmr.action.Action#setActionCondition(int, org.alfresco.service.cmr.action.ActionCondition) + */ + public void setActionCondition(int index, ActionCondition actionCondition) + { + this.actionConditions.set(index, actionCondition); + } + + /** + * @see org.alfresco.service.cmr.action.Action#removeActionCondition(org.alfresco.service.cmr.action.ActionCondition) + */ + public void removeActionCondition(ActionCondition actionCondition) + { + this.actionConditions.remove(actionCondition); + } + + /** + * @see org.alfresco.service.cmr.action.Action#removeAllActionConditions() + */ + public void removeAllActionConditions() + { + this.actionConditions.clear(); + } + + /** + * Set the action chain + * + * @param actionChain the list of actions that lead to this action + */ + public void setActionChain(Set actionChain) + { + this.actionChain = actionChain; + } + + /** + * Get the action chain + * + * @return the list of actions that lead to this action + */ + public Set getActionChain() + { + return actionChain; + } +} diff --git a/source/java/org/alfresco/repo/action/ActionImplTest.java b/source/java/org/alfresco/repo/action/ActionImplTest.java new file mode 100644 index 0000000000..85bfbf764c --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionImplTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.List; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.util.GUID; + +/** + * @author Roy Wetherall + */ +public class ActionImplTest extends BaseParameterizedItemImplTest +{ + private static final String ID_COND1 = "idCond1"; + private static final String ID_COND2 = "idCond2"; + private static final String ID_COND3 = "idCond3"; + private static final String NAME_COND1 = "nameCond1"; + private static final String NAME_COND2 = "nameCond2"; + private static final String NAME_COND3 = "nameCond3"; + + /** + * @see org.alfresco.repo.rule.common.RuleItemImplTest#create() + */ + @Override + protected ParameterizedItemImpl create() + { + return new ActionImpl( + ID, + NAME, + null, + this.paramValues); + } + + public void testGetRuleActionDefintion() + { + Action temp = (Action)create(); + assertEquals(NAME, temp.getActionDefinitionName()); + } + + public void testSimpleProperties() + { + Action action = (Action)create(); + + // Check the default values + assertFalse(action.getExecuteAsychronously()); + assertNull(action.getCompensatingAction()); + + // Set some values + action.setTitle("title"); + action.setDescription("description"); + action.setExecuteAsynchronously(true); + Action compensatingAction = new ActionImpl(GUID.generate(), "actionDefintionName", null); + action.setCompensatingAction(compensatingAction); + + // Check the values have been set + assertEquals("title", action.getTitle()); + assertEquals("description", action.getDescription()); + assertTrue(action.getExecuteAsychronously()); + assertEquals(compensatingAction, action.getCompensatingAction()); + } + + public void testActionConditions() + { + ActionCondition cond1 = new ActionConditionImpl(ID_COND1, NAME_COND1, this.paramValues); + ActionCondition cond2 = new ActionConditionImpl(ID_COND2, NAME_COND2, this.paramValues); + ActionCondition cond3 = new ActionConditionImpl(ID_COND3, NAME_COND3, this.paramValues); + + Action action = (Action)create(); + + // Check has no conditions + assertFalse(action.hasActionConditions()); + List noConditions = action.getActionConditions(); + assertNotNull(noConditions); + assertEquals(0, noConditions.size()); + + // Add the conditions to the action + action.addActionCondition(cond1); + action.addActionCondition(cond2); + action.addActionCondition(cond3); + + // Check that the conditions are there and in the correct order + assertTrue(action.hasActionConditions()); + List actionConditions = action.getActionConditions(); + assertNotNull(actionConditions); + assertEquals(3, actionConditions.size()); + int counter = 0; + for (ActionCondition condition : actionConditions) + { + if (counter == 0) + { + assertEquals(cond1, condition); + } + else if (counter == 1) + { + assertEquals(cond2, condition); + } + else if (counter == 2) + { + assertEquals(cond3, condition); + } + counter+=1; + } + assertEquals(cond1, action.getActionCondition(0)); + assertEquals(cond2, action.getActionCondition(1)); + assertEquals(cond3, action.getActionCondition(2)); + + // Check remove + action.removeActionCondition(cond3); + assertEquals(2, action.getActionConditions().size()); + + // Check set + action.setActionCondition(1, cond3); + assertEquals(cond1, action.getActionCondition(0)); + assertEquals(cond3, action.getActionCondition(1)); + + // Check index of + assertEquals(0, action.indexOfActionCondition(cond1)); + assertEquals(1, action.indexOfActionCondition(cond3)); + + // Test insert + action.addActionCondition(1, cond2); + assertEquals(3, action.getActionConditions().size()); + assertEquals(cond1, action.getActionCondition(0)); + assertEquals(cond2, action.getActionCondition(1)); + assertEquals(cond3, action.getActionCondition(2)); + + // Check remote all + action.removeAllActionConditions(); + assertFalse(action.hasActionConditions()); + assertEquals(0, action.getActionConditions().size()); + } +} diff --git a/source/java/org/alfresco/repo/action/ActionModel.java b/source/java/org/alfresco/repo/action/ActionModel.java new file mode 100644 index 0000000000..37b3aa0bbc --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionModel.java @@ -0,0 +1,34 @@ +package org.alfresco.repo.action; + +import org.alfresco.service.namespace.QName; + +public interface ActionModel +{ + static final String ACTION_MODEL_URI = "http://www.alfresco.org/model/action/1.0"; + static final String ACTION_MODEL_PREFIX = "act"; + static final QName TYPE_ACTION = QName.createQName(ACTION_MODEL_URI, "action"); + static final QName PROP_DEFINITION_NAME = QName.createQName(ACTION_MODEL_URI, "definitionName"); + static final QName PROP_ACTION_TITLE = QName.createQName(ACTION_MODEL_URI, "actionTitle"); + static final QName PROP_ACTION_DESCRIPTION = QName.createQName(ACTION_MODEL_URI, "actionDescription"); + static final QName PROP_EXECUTE_ASYNCHRONOUSLY = QName.createQName(ACTION_MODEL_URI, "executeAsynchronously"); + static final QName ASSOC_CONDITIONS = QName.createQName(ACTION_MODEL_URI, "conditions"); + static final QName ASSOC_COMPENSATING_ACTION = QName.createQName(ACTION_MODEL_URI, "compensatingAction"); + static final QName ASSOC_PARAMETERS = QName.createQName(ACTION_MODEL_URI, "parameters"); + static final QName TYPE_ACTION_CONDITION = QName.createQName(ACTION_MODEL_URI, "actioncondition"); + static final QName TYPE_ACTION_PARAMETER = QName.createQName(ACTION_MODEL_URI, "actionparameter"); + static final QName PROP_PARAMETER_NAME = QName.createQName(ACTION_MODEL_URI, "parameterName"); + static final QName PROP_PARAMETER_VALUE = QName.createQName(ACTION_MODEL_URI, "parameterValue"); + static final QName TYPE_COMPOSITE_ACTION = QName.createQName(ACTION_MODEL_URI, "compositeaction"); + static final QName ASSOC_ACTIONS = QName.createQName(ACTION_MODEL_URI, "actions"); + + static final QName ASPECT_ACTIONS = QName.createQName(ACTION_MODEL_URI, "actions"); + static final QName ASSOC_ACTION_FOLDER = QName.createQName(ACTION_MODEL_URI, "actionFolder"); + + //static final QName ASPECT_ACTIONABLE = QName.createQName(ACTION_MODEL_URI, "actionable"); + //static final QName ASSOC_SAVED_ACTION_FOLDERS = QName.createQName(ACTION_MODEL_URI, "savedActionFolders"); + //static final QName TYPE_SAVED_ACTION_FOLDER = QName.createQName(ACTION_MODEL_URI, "savedactionfolder"); + //static final QName ASSOC_SAVED_ACTIONS = QName.createQName(ACTION_MODEL_URI, "savedActions"); + + static final QName PROP_CONDITION_INVERT = QName.createQName(ACTION_MODEL_URI, "invert"); + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/action/ActionServiceImpl.java b/source/java/org/alfresco/repo/action/ActionServiceImpl.java new file mode 100644 index 0000000000..73301c7517 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionServiceImpl.java @@ -0,0 +1,1233 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.ActionConditionEvaluator; +import org.alfresco.repo.action.executer.ActionExecuter; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionConditionDefinition; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.action.ParameterizedItem; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Action service implementation + * + * @author Roy Wetherall + */ +public class ActionServiceImpl implements ActionService, RuntimeActionService, ApplicationContextAware +{ + /** + * Transaction resource name + */ + private static final String POST_TRANSACTION_PENDING_ACTIONS = "postTransactionPendingActions"; + + /** + * Error message + */ + private static final String ERR_FAIL = "The action failed to execute due to an error."; + + /** Action assoc name */ + private static final QName ASSOC_NAME_ACTIONS = QName.createQName(ActionModel.ACTION_MODEL_URI, "actions"); + + /** + * The logger + */ + private static Log logger = LogFactory.getLog(ActionServiceImpl.class); + + /** + * Thread local containing the current action chain + */ + ThreadLocal> currentActionChain = new ThreadLocal>(); + + /** + * The application context + */ + private ApplicationContext applicationContext; + + /** + * The transacton service + */ + private TransactionService transactionService; + + /** + * The policy component + */ + private PolicyComponent policyComponent; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The search service + */ + private SearchService searchService; + + /** + * The asynchronous action execution queue + */ + private AsynchronousActionExecutionQueue asynchronousActionExecutionQueue; + + /** + * Action transaction listener + */ + private ActionTransactionListener transactionListener = new ActionTransactionListener(this); + + /** + * All the condition definitions currently registered + */ + private Map conditionDefinitions = new HashMap(); + + /** + * All the action definitions currently registered + */ + private Map actionDefinitions = new HashMap(); + + /** + * Set the application context + * + * @param applicationContext the application context + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + /** + * Set the policy component + * + * @param policyComponent the policy component to register with + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the search service + * + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Set the transaction service + * + * @param transactionService the transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the asynchronous action execution queue + * + * @param asynchronousActionExecutionQueue the asynchronous action execution queue + */ + public void setAsynchronousActionExecutionQueue( + AsynchronousActionExecutionQueue asynchronousActionExecutionQueue) + { + this.asynchronousActionExecutionQueue = asynchronousActionExecutionQueue; + } + + /** + * Get the asychronous action execution queue + * + * @return the asynchronous action execution queue + */ + public AsynchronousActionExecutionQueue getAsynchronousActionExecutionQueue() + { + return asynchronousActionExecutionQueue; + } + + /** + * Initialise methods called by Spring framework + */ + public void initialise() + { + } + + /** + * Gets the saved action folder reference + * + * @param nodeRef the node reference + * @return the node reference + */ + private NodeRef getSavedActionFolderRef(NodeRef nodeRef) + { + List assocs = this.nodeService.getChildAssocs( + nodeRef, + RegexQNamePattern.MATCH_ALL, + ActionModel.ASSOC_ACTION_FOLDER); + if (assocs.size() != 1) + { + throw new ActionServiceException("Unable to retrieve the saved action folder reference."); + } + + return assocs.get(0).getChildRef(); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#getActionDefinition(java.lang.String) + */ + public ActionDefinition getActionDefinition(String name) + { + return this.actionDefinitions.get(name); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#getActionDefinitions() + */ + public List getActionDefinitions() + { + return new ArrayList(this.actionDefinitions.values()); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#getActionConditionDefinition(java.lang.String) + */ + public ActionConditionDefinition getActionConditionDefinition(String name) + { + return this.conditionDefinitions.get(name); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#getActionConditionDefinitions() + */ + public List getActionConditionDefinitions() + { + return new ArrayList(this.conditionDefinitions.values()); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#createActionCondition(java.lang.String) + */ + public ActionCondition createActionCondition(String name) + { + return new ActionConditionImpl(GUID.generate(), name); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#createActionCondition(java.lang.String, java.util.Map) + */ + public ActionCondition createActionCondition(String name, Map params) + { + ActionCondition condition = createActionCondition(name); + condition.setParameterValues(params); + return condition; + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#createAction() + */ + public Action createAction(String name) + { + return new ActionImpl(GUID.generate(),name, null); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#createAction(java.lang.String, java.util.Map) + */ + public Action createAction(String name, Map params) + { + Action action = createAction(name); + action.setParameterValues(params); + return action; + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#createCompositeAction() + */ + public CompositeAction createCompositeAction() + { + return new CompositeActionImpl(GUID.generate(), null); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#evaluateAction(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateAction(Action action, NodeRef actionedUponNodeRef) + { + boolean result = true; + + if (action.hasActionConditions() == true) + { + List actionConditions = action.getActionConditions(); + for (ActionCondition condition : actionConditions) + { + result = result && evaluateActionCondition(condition, actionedUponNodeRef); + } + } + + return result; + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#evaluateActionCondition(org.alfresco.service.cmr.action.ActionCondition, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateActionCondition(ActionCondition condition, NodeRef actionedUponNodeRef) + { + boolean result = false; + + // Evaluate the condition + ActionConditionEvaluator evaluator = (ActionConditionEvaluator)this.applicationContext.getBean(condition.getActionConditionDefinitionName()); + result = evaluator.evaluate(condition, actionedUponNodeRef); + + return result; + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#executeAction(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + public void executeAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions) + { + executeAction(action, actionedUponNodeRef, checkConditions, action.getExecuteAsychronously()); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#executeAction(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + public void executeAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions, boolean executeAsychronously) + { + Set actionChain = this.currentActionChain.get(); + + if (executeAsychronously == false) + { + executeActionImpl(action, actionedUponNodeRef, checkConditions, false, actionChain); + } + else + { + // Add to the post transaction pending action list + addPostTransactionPendingAction(action, actionedUponNodeRef, checkConditions, actionChain); + } + } + + /** + * @see org.alfresco.repo.action.RuntimeActionService#executeActionImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef, boolean, org.alfresco.service.cmr.repository.NodeRef) + */ + public void executeActionImpl( + Action action, + NodeRef actionedUponNodeRef, + boolean checkConditions, + boolean executedAsynchronously, + Set actionChain) + { + if (logger.isDebugEnabled() == true) + { + StringBuilder builder = new StringBuilder("Exceute action impl action chain = "); + if (actionChain == null) + { + builder.append("null"); + } + else + { + for (String value : actionChain) + { + builder.append(value).append(" "); + } + } + logger.debug(builder.toString()); + logger.debug("Current action = " + action.getId()); + } + + if (actionChain == null || actionChain.contains(action.getId()) == false) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Doing executeActionImpl"); + } + + try + { + //Set currentActionChain = this.currentActionChain.get(); + Set origActionChain = null; + + if (actionChain == null) + { + actionChain = new HashSet(); + } + else + { + origActionChain = new HashSet(actionChain); + } + actionChain.add(action.getId()); + this.currentActionChain.set(actionChain); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Adding " + action.getId() + " to action chain."); + } + + try + { + // Check and execute now + if (checkConditions == false || evaluateAction(action, actionedUponNodeRef) == true) + { + // Execute the action + directActionExecution(action, actionedUponNodeRef); + } + } + finally + { + if (origActionChain == null) + { + this.currentActionChain.remove(); + } + else + { + this.currentActionChain.set(origActionChain); + } + + if (logger.isDebugEnabled() == true) + { + logger.debug("Resetting the action chain."); + } + } + } + catch (Throwable exception) + { + // Log the exception + logger.error( + "An error was encountered whilst executing the action '" + action.getActionDefinitionName() + "'.", + exception); + + if (executedAsynchronously == true) + { + // If one is specified, queue the compensating action ready for execution + Action compensatingAction = action.getCompensatingAction(); + if (compensatingAction != null) + { + // Queue the compensating action ready for execution + this.asynchronousActionExecutionQueue.executeAction(this, compensatingAction, actionedUponNodeRef, false, null); + } + } + + // Rethrow the exception + if (exception instanceof RuntimeException) + { + throw (RuntimeException)exception; + } + else + { + throw new ActionServiceException(ERR_FAIL, exception); + } + + } + } + } + + /** + * @see org.alfresco.repo.action.RuntimeActionService#directActionExecution(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + public void directActionExecution(Action action, NodeRef actionedUponNodeRef) + { + // Get the action executer and execute + ActionExecuter executer = (ActionExecuter)this.applicationContext.getBean(action.getActionDefinitionName()); + executer.execute(action, actionedUponNodeRef); + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#executeAction(org.alfresco.service.cmr.action.Action, NodeRef) + */ + public void executeAction(Action action, NodeRef actionedUponNodeRef) + { + executeAction(action, actionedUponNodeRef, true); + } + + /** + * @see org.alfresco.repo.action.RuntimeActionService#registerActionConditionEvaluator(org.alfresco.repo.action.evaluator.ActionConditionEvaluator) + */ + public void registerActionConditionEvaluator(ActionConditionEvaluator actionConditionEvaluator) + { + ActionConditionDefinition cond = actionConditionEvaluator.getActionConditionDefintion(); + this.conditionDefinitions.put(cond.getName(), cond); + } + + /** + * @see org.alfresco.repo.action.RuntimeActionService#registerActionExecuter(org.alfresco.repo.action.executer.ActionExecuter) + */ + public void registerActionExecuter(ActionExecuter actionExecuter) + { + ActionDefinition action = actionExecuter.getActionDefinition(); + this.actionDefinitions.put(action.getName(), action); + } + + /** + * Gets the action node ref from the action id + * + * @param nodeRef the node reference + * @param actionId the acition id + * @return the action node reference + */ + private NodeRef getActionNodeRefFromId(NodeRef nodeRef, String actionId) + { + NodeRef result = null; + + if (this.nodeService.hasAspect(nodeRef, ActionModel.ASPECT_ACTIONS) == true) + { + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(); + namespacePrefixResolver.registerNamespace(NamespaceService.SYSTEM_MODEL_PREFIX, NamespaceService.SYSTEM_MODEL_1_0_URI); + + List nodeRefs = searchService.selectNodes( + getSavedActionFolderRef(nodeRef), + "*[@sys:" + ContentModel.PROP_NODE_UUID.getLocalName() + "='" + actionId + "']", + null, + namespacePrefixResolver, + false); + if (nodeRefs.size() != 0) + { + result = nodeRefs.get(0); + } + } + + return result; + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#saveAction(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.action.Action) + */ + public void saveAction(NodeRef nodeRef, Action action) + { + NodeRef actionNodeRef = getActionNodeRefFromId(nodeRef, action.getId()); + if (actionNodeRef == null) + { + if (this.nodeService.hasAspect(nodeRef, ActionModel.ASPECT_ACTIONS) == false) + { + // Apply the actionable aspect + this.nodeService.addAspect(nodeRef, ActionModel.ASPECT_ACTIONS, null); + } + + Map props = new HashMap(2); + props.put(ActionModel.PROP_DEFINITION_NAME, action.getActionDefinitionName()); + props.put(ContentModel.PROP_NODE_UUID, action.getId()); + + QName actionType = ActionModel.TYPE_ACTION; + if(action instanceof CompositeAction) + { + actionType = ActionModel.TYPE_COMPOSITE_ACTION; + } + + // Create the action node + actionNodeRef = this.nodeService.createNode( + getSavedActionFolderRef(nodeRef), + ContentModel.ASSOC_CONTAINS, + ASSOC_NAME_ACTIONS, + actionType, + props).getChildRef(); + + // Update the created details + ((ActionImpl)action).setCreator((String)this.nodeService.getProperty(actionNodeRef, ContentModel.PROP_CREATOR)); + ((ActionImpl)action).setCreatedDate((Date)this.nodeService.getProperty(actionNodeRef, ContentModel.PROP_CREATED)); + } + + saveActionImpl(nodeRef, actionNodeRef, action); + } + + /** + * @see org.alfresco.repo.action.RuntimeActionService#saveActionImpl(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.action.Action) + */ + public void saveActionImpl(NodeRef owningNodeRef, NodeRef actionNodeRef, Action action) + { + // Set the owning node ref + ((ActionImpl)action).setOwningNodeRef(owningNodeRef); + + // Save action properties + saveActionProperties(actionNodeRef, action); + + // Update the parameters of the action + saveParameters(actionNodeRef, action); + + // Update the conditions of the action + saveConditions(actionNodeRef, action); + + if (action instanceof CompositeAction) + { + // Update composite action + saveActions(actionNodeRef, (CompositeAction)action); + } + + // Update the modified details + ((ActionImpl)action).setModifier((String)this.nodeService.getProperty(actionNodeRef, ContentModel.PROP_MODIFIER)); + ((ActionImpl)action).setModifiedDate((Date)this.nodeService.getProperty(actionNodeRef, ContentModel.PROP_MODIFIED)); + } + + /** + * Save the action property values + * + * @param actionNodeRef the action node reference + * @param action the action + */ + private void saveActionProperties(NodeRef actionNodeRef, Action action) + { + // Update the action property values + Map props = this.nodeService.getProperties(actionNodeRef); + props.put(ActionModel.PROP_ACTION_TITLE, action.getTitle()); + props.put(ActionModel.PROP_ACTION_DESCRIPTION, action.getDescription()); + props.put(ActionModel.PROP_EXECUTE_ASYNCHRONOUSLY, action.getExecuteAsychronously()); + this.nodeService.setProperties(actionNodeRef, props); + + // Update the compensating action (model should enforce the singularity of this association) + Action compensatingAction = action.getCompensatingAction(); + List assocs = this.nodeService.getChildAssocs(actionNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_COMPENSATING_ACTION); + if (assocs.size() == 0) + { + if (compensatingAction != null) + { + Map props2 = new HashMap(2); + props2.put(ActionModel.PROP_DEFINITION_NAME, compensatingAction.getActionDefinitionName()); + props2.put(ContentModel.PROP_NODE_UUID, compensatingAction.getId()); + + NodeRef compensatingActionNodeRef = this.nodeService.createNode( + actionNodeRef, + ActionModel.ASSOC_COMPENSATING_ACTION, + ActionModel.ASSOC_COMPENSATING_ACTION, + ActionModel.TYPE_ACTION, + props2).getChildRef(); + + saveActionImpl(compensatingAction.getOwningNodeRef(), compensatingActionNodeRef, compensatingAction); + } + } + else + { + ChildAssociationRef assoc = assocs.get(0); + if (compensatingAction == null) + { + this.nodeService.removeChild(actionNodeRef, assoc.getChildRef()); + } + else + { + saveActionImpl(compensatingAction.getOwningNodeRef(), assoc.getChildRef(), compensatingAction); + } + } + } + + /** + * Save the actions of a composite action + * + * @param compositeActionNodeRef the node reference of the coposite action + * @param compositeAction the composite action + */ + private void saveActions(NodeRef compositeActionNodeRef, CompositeAction compositeAction) + { + // TODO Need a way of sorting the order of the actions + + Map idToAction = new HashMap(); + List orderedIds = new ArrayList(); + for (Action action : compositeAction.getActions()) + { + idToAction.put(action.getId(), action); + orderedIds.add(action.getId()); + } + + List actionRefs = this.nodeService.getChildAssocs(compositeActionNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_ACTIONS); + for (ChildAssociationRef actionRef : actionRefs) + { + NodeRef actionNodeRef = actionRef.getChildRef(); + if (idToAction.containsKey(actionNodeRef.getId()) == false) + { + // Delete the action + this.nodeService.removeChild(compositeActionNodeRef, actionNodeRef); + } + else + { + // Update the action + Action action = idToAction.get(actionNodeRef.getId()); + saveActionImpl(action.getOwningNodeRef(), actionNodeRef, action); + orderedIds.remove(actionNodeRef.getId()); + } + + } + + // Create the actions remaining + for (String actionId : orderedIds) + { + Action action = idToAction.get(actionId); + + Map props = new HashMap(2); + props.put(ActionModel.PROP_DEFINITION_NAME, action.getActionDefinitionName()); + props.put(ContentModel.PROP_NODE_UUID, action.getId()); + + NodeRef actionNodeRef = this.nodeService.createNode( + compositeActionNodeRef, + ActionModel.ASSOC_ACTIONS, + ActionModel.ASSOC_ACTIONS, + ActionModel.TYPE_ACTION, + props).getChildRef(); + + saveActionImpl(action.getOwningNodeRef(), actionNodeRef, action); + } + } + + /** + * Saves the conditions associated with an action + * + * @param actionNodeRef the action node reference + * @param action the action + */ + private void saveConditions(NodeRef actionNodeRef, Action action) + { + // TODO Need a way of sorting out the order of the conditions + + Map idToCondition = new HashMap(); + List orderedIds = new ArrayList(); + for (ActionCondition actionCondition : action.getActionConditions()) + { + idToCondition.put(actionCondition.getId(), actionCondition); + orderedIds.add(actionCondition.getId()); + } + + List conditionRefs = this.nodeService.getChildAssocs(actionNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_CONDITIONS); + for (ChildAssociationRef conditionRef : conditionRefs) + { + NodeRef conditionNodeRef = conditionRef.getChildRef(); + if (idToCondition.containsKey(conditionNodeRef.getId()) == false) + { + // Delete the condition + this.nodeService.removeChild(actionNodeRef, conditionNodeRef); + } + else + { + saveConditionProperties(conditionNodeRef, idToCondition.get(conditionNodeRef.getId())); + + // Update the conditions parameters + saveParameters(conditionNodeRef, idToCondition.get(conditionNodeRef.getId())); + orderedIds.remove(conditionNodeRef.getId()); + } + + } + + // Create the conditions remaining + for (String nextId : orderedIds) + { + ActionCondition actionCondition = idToCondition.get(nextId); + Map props = new HashMap(2); + props.put(ActionModel.PROP_DEFINITION_NAME, actionCondition.getActionConditionDefinitionName()); + props.put(ContentModel.PROP_NODE_UUID, actionCondition.getId()); + + NodeRef conditionNodeRef = this.nodeService.createNode( + actionNodeRef, + ActionModel.ASSOC_CONDITIONS, + ActionModel.ASSOC_CONDITIONS, + ActionModel.TYPE_ACTION_CONDITION, + props).getChildRef(); + + saveConditionProperties(conditionNodeRef, actionCondition); + saveParameters(conditionNodeRef, actionCondition); + } + } + + /** + * Save the condition properties + * + * @param conditionNodeRef + * @param condition + */ + private void saveConditionProperties(NodeRef conditionNodeRef, ActionCondition condition) + { + this.nodeService.setProperty(conditionNodeRef, ActionModel.PROP_CONDITION_INVERT, condition.getInvertCondition()); + + } + + /** + * Saves the parameters associated with an action or condition + * + * @param parameterizedNodeRef the parameterized item node reference + * @param item the parameterized item + */ + private void saveParameters(NodeRef parameterizedNodeRef, ParameterizedItem item) + { + Map parameterMap = new HashMap(); + parameterMap.putAll(item.getParameterValues()); + + List parameters = this.nodeService.getChildAssocs(parameterizedNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_PARAMETERS); + for (ChildAssociationRef ref : parameters) + { + NodeRef paramNodeRef = ref.getChildRef(); + Map nodeRefParameterMap = this.nodeService.getProperties(paramNodeRef); + String paramName = (String)nodeRefParameterMap.get(ActionModel.PROP_PARAMETER_NAME); + if (parameterMap.containsKey(paramName) == false) + { + // Delete parameter from node ref + this.nodeService.removeChild(parameterizedNodeRef, paramNodeRef); + } + else + { + // Update the parameter value + nodeRefParameterMap.put(ActionModel.PROP_PARAMETER_VALUE, parameterMap.get(paramName)); + this.nodeService.setProperties(paramNodeRef, nodeRefParameterMap); + parameterMap.remove(paramName); + } + } + + // Add any remaing parameters + for (Map.Entry entry : parameterMap.entrySet()) + { + Map nodeRefProperties = new HashMap(2); + nodeRefProperties.put(ActionModel.PROP_PARAMETER_NAME, entry.getKey()); + nodeRefProperties.put(ActionModel.PROP_PARAMETER_VALUE, entry.getValue()); + + this.nodeService.createNode( + parameterizedNodeRef, + ActionModel.ASSOC_PARAMETERS, + ActionModel.ASSOC_PARAMETERS, + ActionModel.TYPE_ACTION_PARAMETER, + nodeRefProperties); + } + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#getActions(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getActions(NodeRef nodeRef) + { + List result = new ArrayList(); + + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, ActionModel.ASPECT_ACTIONS) == true) + { + List actions = this.nodeService.getChildAssocs( + getSavedActionFolderRef(nodeRef), + RegexQNamePattern.MATCH_ALL, ASSOC_NAME_ACTIONS); + for (ChildAssociationRef action : actions) + { + NodeRef actionNodeRef = action.getChildRef(); + result.add(createAction(nodeRef, actionNodeRef)); + } + } + + return result; + } + + /** + * Create an action from the action node reference + * + * @param actionNodeRef the action node reference + * @return the action + */ + private Action createAction(NodeRef owningNodeRef, NodeRef actionNodeRef) + { + Action result = null; + + Map properties = this.nodeService.getProperties(actionNodeRef); + + QName actionType = this.nodeService.getType(actionNodeRef); + if (ActionModel.TYPE_COMPOSITE_ACTION.equals(actionType) == true) + { + // Create a composite action + result = new CompositeActionImpl(actionNodeRef.getId(), owningNodeRef); + populateCompositeAction(actionNodeRef, (CompositeAction)result); + } + else + { + // Create an action + result = new ActionImpl(actionNodeRef.getId(), (String)properties.get(ActionModel.PROP_DEFINITION_NAME), owningNodeRef); + populateAction(actionNodeRef, result); + } + + return result; + } + + /** + * Populate the details of the action from the node reference + * + * @param actionNodeRef the action node reference + * @param action the action + */ + private void populateAction(NodeRef actionNodeRef, Action action) + { + // Populate the action properties + populateActionProperties(actionNodeRef, action); + + // Set the parameters + populateParameters(actionNodeRef, action); + + // Set the conditions + List conditions = this.nodeService.getChildAssocs(actionNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_CONDITIONS); + for (ChildAssociationRef condition : conditions) + { + NodeRef conditionNodeRef = condition.getChildRef(); + action.addActionCondition(createActionCondition(conditionNodeRef)); + } + } + + /** + * Populates the action properties from the node reference + * + * @param actionNodeRef the action node reference + * @param action the action + */ + private void populateActionProperties(NodeRef actionNodeRef, Action action) + { + Map props = this.nodeService.getProperties(actionNodeRef); + + action.setTitle((String)props.get(ActionModel.PROP_ACTION_TITLE)); + action.setDescription((String)props.get(ActionModel.PROP_ACTION_DESCRIPTION)); + + boolean value = false; + Boolean executeAsynchronously = (Boolean)props.get(ActionModel.PROP_EXECUTE_ASYNCHRONOUSLY); + if (executeAsynchronously != null) + { + value = executeAsynchronously.booleanValue(); + } + action.setExecuteAsynchronously(value); + + ((ActionImpl)action).setCreator((String)props.get(ContentModel.PROP_CREATOR)); + ((ActionImpl)action).setCreatedDate((Date)props.get(ContentModel.PROP_CREATED)); + ((ActionImpl)action).setModifier((String)props.get(ContentModel.PROP_MODIFIER)); + ((ActionImpl)action).setModifiedDate((Date)props.get(ContentModel.PROP_MODIFIED)); + + // Get the compensating action + List assocs = this.nodeService.getChildAssocs(actionNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_COMPENSATING_ACTION); + if (assocs.size() != 0) + { + Action compensatingAction = createAction(action.getOwningNodeRef(), assocs.get(0).getChildRef()); + action.setCompensatingAction(compensatingAction); + } + } + + /** + * Populate the parameteres of a parameterized item from the parameterized item node reference + * + * @param parameterizedItemNodeRef the parameterized item node reference + * @param parameterizedItem the parameterized item + */ + private void populateParameters(NodeRef parameterizedItemNodeRef, ParameterizedItem parameterizedItem) + { + List parameters = this.nodeService.getChildAssocs(parameterizedItemNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_PARAMETERS); + for (ChildAssociationRef parameter : parameters) + { + NodeRef parameterNodeRef = parameter.getChildRef(); + Map properties = this.nodeService.getProperties(parameterNodeRef); + parameterizedItem.setParameterValue( + (String)properties.get(ActionModel.PROP_PARAMETER_NAME), + properties.get(ActionModel.PROP_PARAMETER_VALUE)); + } + } + + /** + * Creates an action condition from an action condition node reference + * + * @param conditionNodeRef the condition node reference + * @return the action condition + */ + private ActionCondition createActionCondition(NodeRef conditionNodeRef) + { + Map properties = this.nodeService.getProperties(conditionNodeRef); + ActionCondition condition = new ActionConditionImpl(conditionNodeRef.getId(), (String)properties.get(ActionModel.PROP_DEFINITION_NAME)); + + boolean value = false; + Boolean invert = (Boolean)this.nodeService.getProperty(conditionNodeRef, ActionModel.PROP_CONDITION_INVERT); + if (invert != null) + { + value = invert.booleanValue(); + } + condition.setInvertCondition(value); + + populateParameters(conditionNodeRef, condition); + return condition; + } + + /** + * Populates a composite action from a composite action node reference + * + * @param compositeNodeRef the composite action node reference + * @param compositeAction the composite action + */ + public void populateCompositeAction(NodeRef compositeNodeRef, CompositeAction compositeAction) + { + populateAction(compositeNodeRef, compositeAction); + + List actions = this.nodeService.getChildAssocs(compositeNodeRef, RegexQNamePattern.MATCH_ALL, ActionModel.ASSOC_ACTIONS); + for (ChildAssociationRef action : actions) + { + NodeRef actionNodeRef = action.getChildRef(); + compositeAction.addAction(createAction(compositeAction.getOwningNodeRef(), actionNodeRef)); + } + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#getAction(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public Action getAction(NodeRef nodeRef, String actionId) + { + Action result = null; + + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, ActionModel.ASPECT_ACTIONS) == true) + { + NodeRef actionNodeRef = getActionNodeRefFromId(nodeRef, actionId); + if (actionNodeRef != null) + { + result = createAction(nodeRef, actionNodeRef); + } + } + + return result; + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#removeAction(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.action.Action) + */ + public void removeAction(NodeRef nodeRef, Action action) + { + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, ActionModel.ASPECT_ACTIONS) == true) + { + NodeRef actionNodeRef = getActionNodeRefFromId(nodeRef, action.getId()); + if (actionNodeRef != null) + { + this.nodeService.removeChild(getSavedActionFolderRef(nodeRef), actionNodeRef); + } + } + } + + /** + * @see org.alfresco.service.cmr.action.ActionService#removeAllActions(org.alfresco.service.cmr.repository.NodeRef) + */ + public void removeAllActions(NodeRef nodeRef) + { + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, ActionModel.ASPECT_ACTIONS) == true) + { + List actions = new ArrayList(this.nodeService.getChildAssocs(getSavedActionFolderRef(nodeRef), RegexQNamePattern.MATCH_ALL, ASSOC_NAME_ACTIONS)); + for (ChildAssociationRef action : actions) + { + this.nodeService.removeChild(getSavedActionFolderRef(nodeRef), action.getChildRef()); + } + } + } + + /** + * Add a pending action to the list to be queued for execution once the transaction is completed. + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + * @param checkConditions indicates whether to check the conditions before execution + */ + @SuppressWarnings("unchecked") + private void addPostTransactionPendingAction( + Action action, + NodeRef actionedUponNodeRef, + boolean checkConditions, + Set actionChain) + { + if (logger.isDebugEnabled() == true) + { + StringBuilder builder = new StringBuilder("addPostTransactionPendingAction action chain = "); + if (actionChain == null) + { + builder.append("null"); + } + else + { + for (String value : actionChain) + { + builder.append(value).append(" "); + } + } + logger.debug(builder.toString()); + logger.debug("Current action = " + action.getId()); + } + + // Don't continue if the action is already in the action chain + if (actionChain == null || actionChain.contains(action.getId()) == false) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Doing addPostTransactionPendingAction"); + } + + // Ensure that the transaction listener is bound to the transaction + AlfrescoTransactionSupport.bindListener(this.transactionListener); + + // Add the pending action to the transaction resource + List pendingActions = (List)AlfrescoTransactionSupport.getResource(POST_TRANSACTION_PENDING_ACTIONS); + if (pendingActions == null) + { + pendingActions = new ArrayList(); + AlfrescoTransactionSupport.bindResource(POST_TRANSACTION_PENDING_ACTIONS, pendingActions); + } + + // Check that action has only been added to the list once + PendingAction pendingAction = new PendingAction(action, actionedUponNodeRef, checkConditions, actionChain); + if (pendingActions.contains(pendingAction) == false) + { + pendingActions.add(pendingAction); + } + } + } + + /** + * @see org.alfresco.repo.action.RuntimeActionService#getPostTransactionPendingActions() + */ + @SuppressWarnings("unchecked") + public List getPostTransactionPendingActions() + { + return (List)AlfrescoTransactionSupport.getResource(POST_TRANSACTION_PENDING_ACTIONS); + } + + /** + * Pending action details class + */ + public class PendingAction + { + /** + * The action + */ + private Action action; + + /** + * The actioned upon node reference + */ + private NodeRef actionedUponNodeRef; + + /** + * Indicates whether the conditions should be checked before the action is executed + */ + private boolean checkConditions; + + private Set actionChain; + + /** + * Constructor + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + * @param checkConditions indicated whether the conditions need to be checked + */ + public PendingAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions, Set actionChain) + { + this.action = action; + this.actionedUponNodeRef = actionedUponNodeRef; + this.checkConditions = checkConditions; + this.actionChain = actionChain; + } + + /** + * Get the action + * + * @return the action + */ + public Action getAction() + { + return action; + } + + /** + * Get the actioned upon node reference + * + * @return the actioned upon node reference + */ + public NodeRef getActionedUponNodeRef() + { + return actionedUponNodeRef; + } + + /** + * Get the check conditions value + * + * @return indicates whether the condition should be checked + */ + public boolean getCheckConditions() + { + return this.checkConditions; + } + + public Set getActionChain() + { + return this.actionChain; + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() + { + int hashCode = 37 * this.actionedUponNodeRef.hashCode(); + hashCode += 37 * this.action.hashCode(); + return hashCode; + } + + /** + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof PendingAction) + { + PendingAction that = (PendingAction) obj; + return (this.action.equals(that.action) && this.actionedUponNodeRef.equals(that.actionedUponNodeRef)); + } + else + { + return false; + } + } + } +} diff --git a/source/java/org/alfresco/repo/action/ActionServiceImplTest.java b/source/java/org/alfresco/repo/action/ActionServiceImplTest.java new file mode 100644 index 0000000000..ab5ca34ce8 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionServiceImplTest.java @@ -0,0 +1,938 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.evaluator.InCategoryEvaluator; +import org.alfresco.repo.action.evaluator.NoConditionEvaluator; +import org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuter; +import org.alfresco.repo.action.executer.CheckInActionExecuter; +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.repo.action.executer.CompositeActionExecuter; +import org.alfresco.repo.action.executer.MoveActionExecuter; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionConditionDefinition; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.BaseSpringTest; + +/** + * Action service test + * + * @author Roy Wetherall + */ +public class ActionServiceImplTest extends BaseAlfrescoSpringTest +{ + private static final String BAD_NAME = "badName"; + + private NodeRef nodeRef; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + this.nodeService.setProperty( + this.nodeRef, + ContentModel.PROP_CONTENT, + new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null)); + } + + /** + * Test getActionDefinition + */ + public void testGetActionDefinition() + { + ActionDefinition action = actionService.getActionDefinition(AddFeaturesActionExecuter.NAME); + assertNotNull(action); + assertEquals(AddFeaturesActionExecuter.NAME, action.getName()); + + ActionConditionDefinition nullCondition = this.actionService.getActionConditionDefinition(BAD_NAME); + assertNull(nullCondition); + } + + /** + * Test getActionDefintions + */ + public void testGetActionDefinitions() + { + List defintions = this.actionService.getActionDefinitions(); + assertNotNull(defintions); + assertFalse(defintions.isEmpty()); + + for (ActionDefinition definition : defintions) + { + System.out.println(definition.getTitle()); + } + } + + /** + * Test getActionConditionDefinition + */ + public void testGetActionConditionDefinition() + { + ActionConditionDefinition condition = this.actionService.getActionConditionDefinition(NoConditionEvaluator.NAME); + assertNotNull(condition); + assertEquals(NoConditionEvaluator.NAME, condition.getName()); + + ActionConditionDefinition nullCondition = this.actionService.getActionConditionDefinition(BAD_NAME); + assertNull(nullCondition); + } + + /** + * Test getActionConditionDefinitions + * + */ + public void testGetActionConditionDefinitions() + { + List defintions = this.actionService.getActionConditionDefinitions(); + assertNotNull(defintions); + assertFalse(defintions.isEmpty()); + + for (ActionConditionDefinition definition : defintions) + { + System.out.println(definition.getTitle()); + } + } + + /** + * Test create action condition + */ + public void testCreateActionCondition() + { + ActionCondition condition = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + assertNotNull(condition); + assertEquals(NoConditionEvaluator.NAME, condition.getActionConditionDefinitionName()); + } + + /** + * Test createAction + */ + public void testCreateAction() + { + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + assertNotNull(action); + assertEquals(AddFeaturesActionExecuter.NAME, action.getActionDefinitionName()); + } + + /** + * Test createCompositeAction + */ + public void testCreateCompositeAction() + { + CompositeAction action = this.actionService.createCompositeAction(); + assertNotNull(action); + assertEquals(CompositeActionExecuter.NAME, action.getActionDefinitionName()); + } + + /** + * Evaluate action + */ + public void testEvaluateAction() + { + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + assertTrue(this.actionService.evaluateAction(action, this.nodeRef)); + + ActionCondition condition = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.doc"); + action.addActionCondition(condition); + + assertFalse(this.actionService.evaluateAction(action, this.nodeRef)); + this.nodeService.setProperty(this.nodeRef, ContentModel.PROP_NAME, "myDocument.doc"); + assertTrue(this.actionService.evaluateAction(action, this.nodeRef)); + + ActionCondition condition2 = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + condition2.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "my"); + action.addActionCondition(condition2); + assertTrue(this.actionService.evaluateAction(action, this.nodeRef)); + + this.nodeService.setProperty(this.nodeRef, ContentModel.PROP_NAME, "document.doc"); + assertFalse(this.actionService.evaluateAction(action, this.nodeRef)); + } + + /** + * Test evaluate action condition + */ + public void testEvaluateActionCondition() + { + ActionCondition condition = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.doc"); + + assertFalse(this.actionService.evaluateActionCondition(condition, this.nodeRef)); + this.nodeService.setProperty(this.nodeRef, ContentModel.PROP_NAME, "myDocument.doc"); + assertTrue(this.actionService.evaluateActionCondition(condition, this.nodeRef)); + + // Check that inverting the condition has the correct effect + condition.setInvertCondition(true); + assertFalse(this.actionService.evaluateActionCondition(condition, this.nodeRef)); + } + + /** + * Test execute action + */ + public void testExecuteAction() + { + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + + this.actionService.executeAction(action, this.nodeRef); + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.nodeService.removeAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + ActionCondition condition = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.doc"); + action.addActionCondition(condition); + + this.actionService.executeAction(action, this.nodeRef); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.actionService.executeAction(action, this.nodeRef, true); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.actionService.executeAction(action, this.nodeRef, false); + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.nodeService.removeAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.nodeService.setProperty(this.nodeRef, ContentModel.PROP_NAME, "myDocument.doc"); + this.actionService.executeAction(action, this.nodeRef); + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.nodeService.removeAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.nodeService.removeAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // Create the composite action + Action action1 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action1.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_LOCKABLE); + Action action2 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action2.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + CompositeAction compAction = this.actionService.createCompositeAction(); + compAction.setTitle("title"); + compAction.setDescription("description"); + compAction.addAction(action1); + compAction.addAction(action2); + + // Execute the composite action + this.actionService.executeAction(compAction, this.nodeRef); + + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE)); + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + } + + public void testGetAndGetAllWithNoActions() + { + assertNull(this.actionService.getAction(this.nodeRef, AddFeaturesActionExecuter.NAME)); + List actions = this.actionService.getActions(this.nodeRef); + assertNotNull(actions); + assertEquals(0, actions.size()); + } + + /** + * Test saving an action with no conditions. Includes testing storage and retrieval + * of compensating actions. + */ + public void testSaveActionNoCondition() + { + // Create the action + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + String actionId = action.getId(); + + // Set the parameters of the action + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + + // Set the title and description of the action + action.setTitle("title"); + action.setDescription("description"); + action.setExecuteAsynchronously(true); + + // Save the action + this.actionService.saveAction(this.nodeRef, action); + + // Get the action + Action savedAction = this.actionService.getAction(this.nodeRef, actionId); + + // Check the action + assertEquals(action.getId(), savedAction.getId()); + assertEquals(action.getActionDefinitionName(), savedAction.getActionDefinitionName()); + + // Check the properties + assertEquals("title", savedAction.getTitle()); + assertEquals("description", savedAction.getDescription()); + assertTrue(savedAction.getExecuteAsychronously()); + + // Check that the compensating action has not been set + assertNull(savedAction.getCompensatingAction()); + + // Check the properties + assertEquals(1, savedAction.getParameterValues().size()); + assertEquals(ContentModel.ASPECT_VERSIONABLE, savedAction.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME)); + + // Check the conditions + assertNotNull(savedAction.getActionConditions()); + assertEquals(0, savedAction.getActionConditions().size()); + + // Edit the properties of the action + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, "testName"); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_PROPERTIES, (Serializable)properties); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_AUDITABLE); + + // Set the compensating action + Action compensatingAction = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + compensatingAction.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + action.setCompensatingAction(compensatingAction); + + this.actionService.saveAction(this.nodeRef, action); + Action savedAction2 = this.actionService.getAction(this.nodeRef, actionId); + + // Check the updated properties + assertEquals(2, savedAction2.getParameterValues().size()); + assertEquals(ContentModel.ASPECT_AUDITABLE, savedAction2.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME)); + Map temp = (Map)savedAction2.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_PROPERTIES); + assertNotNull(temp); + assertEquals(1, temp.size()); + assertEquals("testName", temp.get(ContentModel.PROP_NAME)); + + // Check the compensating action + Action savedCompensatingAction = savedAction2.getCompensatingAction(); + assertNotNull(savedCompensatingAction); + assertEquals(compensatingAction, savedCompensatingAction); + assertEquals(AddFeaturesActionExecuter.NAME, savedCompensatingAction.getActionDefinitionName()); + assertEquals(ContentModel.ASPECT_VERSIONABLE, savedCompensatingAction.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME)); + + // Change the details of the compensating action (edit and remove) + compensatingAction.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_CLASSIFIABLE); + this.actionService.saveAction(this.nodeRef, action); + Action savedAction3 = this.actionService.getAction(this.nodeRef, actionId); + Action savedCompensatingAction2 = savedAction3.getCompensatingAction(); + assertNotNull(savedCompensatingAction2); + assertEquals(compensatingAction, savedCompensatingAction2); + assertEquals(AddFeaturesActionExecuter.NAME, savedCompensatingAction2.getActionDefinitionName()); + assertEquals(ContentModel.ASPECT_CLASSIFIABLE, savedCompensatingAction2.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME)); + action.setCompensatingAction(null); + this.actionService.saveAction(this.nodeRef, action); + Action savedAction4 = this.actionService.getAction(this.nodeRef, actionId); + assertNull(savedAction4.getCompensatingAction()); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + public void testOwningNodeRef() + { + // Create the action + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + String actionId = action.getId(); + + // Set the parameters of the action + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + + // Set the title and description of the action + action.setTitle("title"); + action.setDescription("description"); + action.setExecuteAsynchronously(true); + + // Check the owning node ref + assertNull(action.getOwningNodeRef()); + + // Save the action + this.actionService.saveAction(this.nodeRef, action); + + // Check the owning node ref + assertEquals(this.nodeRef, action.getOwningNodeRef()); + + // Get the action + Action savedAction = this.actionService.getAction(this.nodeRef, actionId); + + // Check the owning node ref + assertEquals(this.nodeRef, savedAction.getOwningNodeRef());; + } + + /** + * Test saving an action with conditions + */ + public void testSaveActionWithConditions() + { + // Create the action + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + String actionId = action.getId(); + + // Set the parameters of the action + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, "testName"); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_PROPERTIES, (Serializable)properties); + + // Set the conditions of the action + ActionCondition actionCondition = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + actionCondition.setInvertCondition(true); + ActionCondition actionCondition2 = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + actionCondition2.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.doc"); + action.addActionCondition(actionCondition); + action.addActionCondition(actionCondition2); + + // Save the action + this.actionService.saveAction(this.nodeRef, action); + + // Get the action + Action savedAction = this.actionService.getAction(this.nodeRef, actionId); + + // Check the action + assertEquals(action.getId(), savedAction.getId()); + assertEquals(action.getActionDefinitionName(), savedAction.getActionDefinitionName()); + + // Check the properties + assertEquals(action.getParameterValues().size(), savedAction.getParameterValues().size()); + assertEquals(ContentModel.ASPECT_VERSIONABLE, savedAction.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME)); + Map temp = (Map)savedAction.getParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_PROPERTIES); + assertNotNull(temp); + assertEquals(1, temp.size()); + assertEquals("testName", temp.get(ContentModel.PROP_NAME)); + + // Check the conditions + assertNotNull(savedAction.getActionConditions()); + assertEquals(2, savedAction.getActionConditions().size()); + for (ActionCondition savedCondition : savedAction.getActionConditions()) + { + if (savedCondition.getActionConditionDefinitionName().equals(NoConditionEvaluator.NAME) == true) + { + assertEquals(0, savedCondition.getParameterValues().size()); + assertTrue(savedCondition.getInvertCondition()); + } + else if (savedCondition.getActionConditionDefinitionName().equals(ComparePropertyValueEvaluator.NAME) == true) + { + assertEquals(1, savedCondition.getParameterValues().size()); + assertEquals("*.doc", savedCondition.getParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE)); + assertFalse(savedCondition.getInvertCondition()); + } + else + { + fail("There is a condition here that we are not expecting."); + } + } + + // Modify the conditions of the action + ActionCondition actionCondition3 = this.actionService.createActionCondition(InCategoryEvaluator.NAME); + actionCondition3.setParameterValue(InCategoryEvaluator.PARAM_CATEGORY_ASPECT, ContentModel.ASPECT_OWNABLE); + action.addActionCondition(actionCondition3); + action.removeActionCondition(actionCondition); + actionCondition2.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.exe"); + actionCondition2.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS); + + this.actionService.saveAction(this.nodeRef, action); + Action savedAction2 = this.actionService.getAction(this.nodeRef, actionId); + + // Check that the conditions have been updated correctly + assertNotNull(savedAction2.getActionConditions()); + assertEquals(2, savedAction2.getActionConditions().size()); + for (ActionCondition savedCondition : savedAction2.getActionConditions()) + { + if (savedCondition.getActionConditionDefinitionName().equals(InCategoryEvaluator.NAME) == true) + { + assertEquals(1, savedCondition.getParameterValues().size()); + assertEquals(ContentModel.ASPECT_OWNABLE, savedCondition.getParameterValue(InCategoryEvaluator.PARAM_CATEGORY_ASPECT)); + } + else if (savedCondition.getActionConditionDefinitionName().equals(ComparePropertyValueEvaluator.NAME) == true) + { + assertEquals(2, savedCondition.getParameterValues().size()); + assertEquals("*.exe", savedCondition.getParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE)); + assertEquals(ComparePropertyValueOperation.EQUALS, savedCondition.getParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION)); + } + else + { + fail("There is a condition here that we are not expecting."); + } + } + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + /** + * Test saving a composite action + */ + public void testSaveCompositeAction() + { + Action action1 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + Action action2 = this.actionService.createAction(CheckInActionExecuter.NAME); + + CompositeAction compositeAction = this.actionService.createCompositeAction(); + String actionId = compositeAction.getId(); + compositeAction.addAction(action1); + compositeAction.addAction(action2); + + this.actionService.saveAction(this.nodeRef, compositeAction); + assertEquals(1, this.actionService.getActions(this.nodeRef).size()); + CompositeAction savedCompositeAction = (CompositeAction)this.actionService.getAction(this.nodeRef, actionId); + + // Check the saved composite action + assertEquals(2, savedCompositeAction.getActions().size()); + for (Action action : savedCompositeAction.getActions()) + { + if (action.getActionDefinitionName().equals(AddFeaturesActionExecuter.NAME) == true) + { + assertEquals(action, action1); + } + else if (action.getActionDefinitionName().equals(CheckInActionExecuter.NAME) == true) + { + assertEquals(action, action2); + } + else + { + fail("We have an action here we are not expecting."); + } + } + + // Change the actions and re-save + compositeAction.removeAction(action1); + Action action3 = this.actionService.createAction(CheckOutActionExecuter.NAME); + compositeAction.addAction(action3); + action2.setParameterValue(CheckInActionExecuter.PARAM_DESCRIPTION, "description"); + + this.actionService.saveAction(this.nodeRef, compositeAction); + assertEquals(1, this.actionService.getActions(this.nodeRef).size()); + CompositeAction savedCompositeAction2 = (CompositeAction)this.actionService.getAction(this.nodeRef, actionId); + + assertEquals(2, savedCompositeAction2.getActions().size()); + for (Action action : savedCompositeAction2.getActions()) + { + if (action.getActionDefinitionName().equals(CheckOutActionExecuter.NAME) == true) + { + assertEquals(action, action3); + } + else if (action.getActionDefinitionName().equals(CheckInActionExecuter.NAME) == true) + { + assertEquals(action, action2); + assertEquals("description", action2.getParameterValue(CheckInActionExecuter.PARAM_DESCRIPTION)); + } + else + { + fail("We have an action here we are not expecting."); + } + } + } + + /** + * Test remove action + */ + public void testRemove() + { + assertEquals(0, this.actionService.getActions(this.nodeRef).size()); + + Action action1 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + this.actionService.saveAction(this.nodeRef, action1); + Action action2 = this.actionService.createAction(CheckInActionExecuter.NAME); + this.actionService.saveAction(this.nodeRef, action2); + assertEquals(2, this.actionService.getActions(this.nodeRef).size()); + + this.actionService.removeAction(this.nodeRef, action1); + assertEquals(1, this.actionService.getActions(this.nodeRef).size()); + + this.actionService.removeAllActions(this.nodeRef); + assertEquals(0, this.actionService.getActions(this.nodeRef).size()); + } + + public void testConditionOrder() + { + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + String actionId = action.getId(); + + ActionCondition condition1 = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + ActionCondition condition2 = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + + action.addActionCondition(condition1); + action.addActionCondition(condition2); + + this.actionService.saveAction(this.nodeRef, action); + Action savedAction = this.actionService.getAction(this.nodeRef, actionId); + + // Check that the conditions have been retrieved in the correct order + assertNotNull(savedAction); + assertEquals(condition1, savedAction.getActionCondition(0)); + assertEquals(condition2, savedAction.getActionCondition(1)); + + ActionCondition condition3 = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + ActionCondition condition4 = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + + // Update the conditions on the action + savedAction.removeActionCondition(condition1); + savedAction.addActionCondition(condition3); + savedAction.addActionCondition(condition4); + + this.actionService.saveAction(this.nodeRef, savedAction); + Action savedAction2 = this.actionService.getAction(this.nodeRef, actionId); + + // Check that the conditions are still in the correct order + assertNotNull(savedAction2); + assertEquals(condition2, savedAction2.getActionCondition(0)); + assertEquals(condition3, savedAction2.getActionCondition(1)); + assertEquals(condition4, savedAction2.getActionCondition(2)); + } + + public void testActionOrder() + { + CompositeAction action = this.actionService.createCompositeAction(); + String actionId = action.getId(); + + Action action1 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + Action action2 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + + action.addAction(action1); + action.addAction(action2); + + this.actionService.saveAction(this.nodeRef, action); + CompositeAction savedAction = (CompositeAction)this.actionService.getAction(this.nodeRef, actionId); + + // Check that the conditions have been retrieved in the correct order + assertNotNull(savedAction); + assertEquals(action1, savedAction.getAction(0)); + assertEquals(action2, savedAction.getAction(1)); + + Action action3 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + Action action4 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + + // Update the conditions on the action + savedAction.removeAction(action1); + savedAction.addAction(action3); + savedAction.addAction(action4); + + this.actionService.saveAction(this.nodeRef, savedAction); + CompositeAction savedAction2 = (CompositeAction)this.actionService.getAction(this.nodeRef, actionId); + + // Check that the conditions are still in the correct order + assertNotNull(savedAction2); + assertEquals(action2, savedAction2.getAction(0)); + assertEquals(action3, savedAction2.getAction(1)); + assertEquals(action4, savedAction2.getAction(2)); + } + + /** =================================================================================== + * Test asynchronous actions + */ + + /** + * Test asynchronous execute action + */ + public void testAsyncExecuteAction() + { + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE)); + + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + action.setExecuteAsynchronously(true); + + this.actionService.executeAction(action, this.nodeRef); + + setComplete(); + endTransaction(); + + final NodeService finalNodeService = this.nodeService; + final NodeRef finalNodeRef = this.nodeRef; + + postAsyncActionTest( + this.transactionService, + 1000, + 10, + new AsyncTest() + { + public boolean executeTest() + { + return ( + finalNodeService.hasAspect(finalNodeRef, ContentModel.ASPECT_VERSIONABLE)); + }; + }); + } + + + + /** + * Test async composite action execution + */ + public void testAsyncCompositeActionExecute() + { + // Create the composite action + Action action1 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action1.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_LOCKABLE); + Action action2 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action2.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + CompositeAction compAction = this.actionService.createCompositeAction(); + compAction.setTitle("title"); + compAction.setDescription("description"); + compAction.addAction(action1); + compAction.addAction(action2); + compAction.setExecuteAsynchronously(true); + + // Execute the composite action + this.actionService.executeAction(compAction, this.nodeRef); + + setComplete(); + endTransaction(); + + final NodeService finalNodeService = this.nodeService; + final NodeRef finalNodeRef = this.nodeRef; + + postAsyncActionTest( + this.transactionService, + 1000, + 10, + new AsyncTest() + { + public boolean executeTest() + { + return ( + finalNodeService.hasAspect(finalNodeRef, ContentModel.ASPECT_VERSIONABLE) && + finalNodeService.hasAspect(finalNodeRef, ContentModel.ASPECT_LOCKABLE)); + }; + }); + } + + public void xtestAsyncLoadTest() + { + // TODO this is very weak .. how do we improve this ??? + + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + action.setExecuteAsynchronously(true); + + for (int i = 0; i < 1000; i++) + { + this.actionService.executeAction(action, this.nodeRef); + } + + setComplete(); + endTransaction(); + + // TODO how do we assess whether the large number of actions stacked cause a problem ?? + } + + /** + * + * @param sleepTime + * @param maxTries + * @param test + * @param context + */ + public static void postAsyncActionTest( + TransactionService transactionService, + final long sleepTime, + final int maxTries, + final AsyncTest test) + { + try + { + int tries = 0; + boolean done = false; + while (done == false && tries < maxTries) + { + try + { + // Increment the tries counter + tries++; + + // Sleep for a bit + Thread.sleep(sleepTime); + + done = (TransactionUtil.executeInUserTransaction( + transactionService, + new TransactionUtil.TransactionWork() + { + public Boolean doWork() + { + // See if the action has been performed + boolean done = test.executeTest(); + return done; + } + })).booleanValue(); + } + catch (InterruptedException e) + { + // Do nothing + e.printStackTrace(); + } + } + + if (done == false) + { + throw new RuntimeException("Asynchronous action was not executed."); + } + } + catch (Throwable exception) + { + exception.printStackTrace(); + fail("An exception was encountered whilst checking the async action was executed: " + exception.getMessage()); + } + } + + /** + * Async test interface + */ + public interface AsyncTest + { + boolean executeTest(); + } + + /** =================================================================================== + * Test failure behaviour + */ + + /** + * Test sync failure behaviour + */ + public void testSyncFailureBehaviour() + { + // Create an action that is going to fail + Action action = this.actionService.createAction(MoveActionExecuter.NAME); + action.setParameterValue(MoveActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + action.setParameterValue(MoveActionExecuter.PARAM_ASSOC_QNAME, ContentModel.ASSOC_CHILDREN); + // Create a bad node ref + NodeRef badNodeRef = new NodeRef(this.storeRef, "123123"); + action.setParameterValue(MoveActionExecuter.PARAM_DESTINATION_FOLDER, badNodeRef); + + try + { + this.actionService.executeAction(action, this.nodeRef); + + // Fail if we get there since the exception should have been raised + fail("An exception should have been raised."); + } + catch (RuntimeException exception) + { + // Good! The exception was raised correctly + } + + // Test what happens when a element of a composite action fails (should raise and bubble up to parent bahviour) + // Create the composite action + Action action1 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action1.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_LOCKABLE); + Action action2 = this.actionService.createAction(AddFeaturesActionExecuter.NAME); + action2.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, QName.createQName("{test}badDogAspect")); + CompositeAction compAction = this.actionService.createCompositeAction(); + compAction.setTitle("title"); + compAction.setDescription("description"); + compAction.addAction(action1); + compAction.addAction(action2); + + try + { + // Execute the composite action + this.actionService.executeAction(compAction, this.nodeRef); + + fail("An exception should have been raised here !!"); + } + catch (RuntimeException runtimeException) + { + // Good! The exception was raised + } + } + + /** + * Test the compensating action + */ + public void testCompensatingAction() + { + // Create an action that is going to fail + final Action action = this.actionService.createAction(MoveActionExecuter.NAME); + action.setParameterValue(MoveActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + action.setParameterValue(MoveActionExecuter.PARAM_ASSOC_QNAME, ContentModel.ASSOC_CHILDREN); + // Create a bad node ref + NodeRef badNodeRef = new NodeRef(this.storeRef, "123123"); + action.setParameterValue(MoveActionExecuter.PARAM_DESTINATION_FOLDER, badNodeRef); + action.setTitle("title"); + + // Create the compensating action + Action compensatingAction = actionService.createAction(AddFeaturesActionExecuter.NAME); + compensatingAction.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_CLASSIFIABLE); + compensatingAction.setTitle("title"); + action.setCompensatingAction(compensatingAction); + + // Set the action to execute asynchronously + action.setExecuteAsynchronously(true); + + this.actionService.executeAction(action, this.nodeRef); + + setComplete(); + endTransaction(); + + postAsyncActionTest( + this.transactionService, + 1000, + 10, + new AsyncTest() + { + public boolean executeTest() + { + return ( + ActionServiceImplTest.this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_CLASSIFIABLE)); + }; + }); + + // Modify the compensating action so that it will also fail + compensatingAction.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, QName.createQName("{test}badAspect")); + + TransactionUtil.executeInUserTransaction( + this.transactionService, + new TransactionUtil.TransactionWork() + { + public Object doWork() + { + try + { + ActionServiceImplTest.this.actionService.executeAction(action, ActionServiceImplTest.this.nodeRef); + } + catch (RuntimeException exception) + { + // The exception should have been ignored and execution continued + exception.printStackTrace(); + fail("An exception should not have been raised here."); + } + return null; + } + + }); + + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/action/ActionTestSuite.java b/source/java/org/alfresco/repo/action/ActionTestSuite.java new file mode 100644 index 0000000000..a39f2fb910 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionTestSuite.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.alfresco.repo.action.evaluator.CompareMimeTypeEvaluatorTest; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluatorTest; +import org.alfresco.repo.action.evaluator.HasAspectEvaluatorTest; +import org.alfresco.repo.action.evaluator.IsSubTypeEvaluatorTest; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuterTest; +import org.alfresco.repo.action.executer.ContentMetadataExtracterTest; +import org.alfresco.repo.action.executer.SetPropertyValueActionExecuterTest; +import org.alfresco.repo.action.executer.SpecialiseTypeActionExecuterTest; + + +/** + * Version test suite + * + * @author Roy Wetherall + */ +public class ActionTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(ParameterDefinitionImplTest.class); + suite.addTestSuite(ActionDefinitionImplTest.class); + suite.addTestSuite(ActionConditionDefinitionImplTest.class); + suite.addTestSuite(ActionImplTest.class); + suite.addTestSuite(ActionConditionImplTest.class); + suite.addTestSuite(CompositeActionImplTest.class); + suite.addTestSuite(ActionServiceImplTest.class); + + // Test evaluators + suite.addTestSuite(IsSubTypeEvaluatorTest.class); + suite.addTestSuite(ComparePropertyValueEvaluatorTest.class); + suite.addTestSuite(CompareMimeTypeEvaluatorTest.class); + suite.addTestSuite(HasAspectEvaluatorTest.class); + + // Test executors + suite.addTestSuite(SetPropertyValueActionExecuterTest.class); + suite.addTestSuite(AddFeaturesActionExecuterTest.class); + suite.addTestSuite(ContentMetadataExtracterTest.class); + suite.addTestSuite(SpecialiseTypeActionExecuterTest.class); + + return suite; + } +} diff --git a/source/java/org/alfresco/repo/action/ActionTransactionListener.java b/source/java/org/alfresco/repo/action/ActionTransactionListener.java new file mode 100644 index 0000000000..0ed201e828 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionTransactionListener.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.alfresco.repo.action.ActionServiceImpl.PendingAction; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.util.GUID; + +/** + * The action service transaction listener + * + * @author Roy Wetherall + */ +public class ActionTransactionListener implements TransactionListener +{ + /** + * Id used in equals and hash + */ + private String id = GUID.generate(); + + /** + * The action service (runtime interface) + */ + private RuntimeActionService actionService; + + /** + * Constructor + * + * @param actionService the action service + */ + public ActionTransactionListener(RuntimeActionService actionService) + { + this.actionService = actionService; + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#flush() + */ + public void flush() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#beforeCommit(boolean) + */ + public void beforeCommit(boolean readOnly) + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#beforeCompletion() + */ + public void beforeCompletion() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#afterCommit() + */ + public void afterCommit() + { + for (PendingAction pendingAction : this.actionService.getPostTransactionPendingActions()) + { + this.actionService.getAsynchronousActionExecutionQueue().executeAction( + actionService, + pendingAction.getAction(), + pendingAction.getActionedUponNodeRef(), + pendingAction.getCheckConditions(), + pendingAction.getActionChain()); + } + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#afterRollback() + */ + public void afterRollback() + { + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() + { + return this.id.hashCode(); + } + + /** + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof ActionTransactionListener) + { + ActionTransactionListener that = (ActionTransactionListener) obj; + return (this.id.equals(that.id)); + } + else + { + return false; + } + } + +} diff --git a/source/java/org/alfresco/repo/action/ActionsAspect.java b/source/java/org/alfresco/repo/action/ActionsAspect.java new file mode 100644 index 0000000000..30e9a575a7 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionsAspect.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.Behaviour; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * Class containing behaviour for the actions aspect + * + * @author Roy Wetherall + */ +public class ActionsAspect +{ + private Behaviour onAddAspectBehaviour; + + private PolicyComponent policyComponent; + + private RuleService ruleService; + + private NodeService nodeService; + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + public void init() + { + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + ActionModel.ASPECT_ACTIONS, + new JavaBehaviour(this, "onCopyNode")); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"), + ActionModel.ASPECT_ACTIONS, + new JavaBehaviour(this, "onCopyComplete")); + + this.onAddAspectBehaviour = new JavaBehaviour(this, "onAddAspect"); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + ActionModel.ASPECT_ACTIONS, + onAddAspectBehaviour); + } + + + + /** + * Helper to diable the on add aspect policy behaviour. Helpful when importing, + * copying and other bulk respstorative operations. + * + * TODO will eventually be redundant when policies can be enabled/diabled in the + * policy componenet + */ + public void disbleOnAddAspect() + { + this.onAddAspectBehaviour.disable(); + } + + /** + * Helper to enable the on add aspect policy behaviour. Helpful when importing, + * copying and other bulk respstorative operations. + * + * TODO will eventually be redundant when policies can be enabled/diabled in the + * policy componenet + */ + public void enableOnAddAspect() + { + this.onAddAspectBehaviour.enable(); + } + + /** + * On add aspect policy behaviour + * @param nodeRef + * @param aspectTypeQName + */ + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + this.ruleService.disableRules(nodeRef); + try + { + this.nodeService.createNode( + nodeRef, + ActionModel.ASSOC_ACTION_FOLDER, + ActionModel.ASSOC_ACTION_FOLDER, + ContentModel.TYPE_SYSTEM_FOLDER); + } + finally + { + this.ruleService.enableRules(nodeRef); + } + } + + public void onCopyNode( + QName classRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + copyDetails.addAspect(ActionModel.ASPECT_ACTIONS); + + List assocs = this.nodeService.getChildAssocs( + sourceNodeRef, + RegexQNamePattern.MATCH_ALL, + ActionModel.ASSOC_ACTION_FOLDER); + for (ChildAssociationRef assoc : assocs) + { + copyDetails.addChildAssociation(classRef, assoc, true); + } + + this.onAddAspectBehaviour.disable(); + } + + public void onCopyComplete( + QName classRef, + NodeRef sourceNodeRef, + NodeRef destinationRef, + Map copyMap) + { + this.onAddAspectBehaviour.enable(); + } +} diff --git a/source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueue.java b/source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueue.java new file mode 100644 index 0000000000..71ca0f7278 --- /dev/null +++ b/source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueue.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.Set; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Asynchronous action execution queue + * + * @author Roy Wetherall + */ +public interface AsynchronousActionExecutionQueue +{ + /** + * + * @param actionedUponNodeRef + * @param action + * @param checkConditions + */ + void executeAction( + RuntimeActionService actionService, + Action action, + NodeRef actionedUponNodeRef, + boolean checkConditions, + Set actionChain); + +} diff --git a/source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueueImpl.java b/source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueueImpl.java new file mode 100644 index 0000000000..39d53d5e2d --- /dev/null +++ b/source/java/org/alfresco/repo/action/AsynchronousActionExecutionQueueImpl.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.transaction.TransactionService; + +/** + * The asynchronous action execution queue implementation + * + * @author Roy Wetherall + */ +public class AsynchronousActionExecutionQueueImpl extends ThreadPoolExecutor implements + AsynchronousActionExecutionQueue +{ + /** + * Default pool values + */ + private static final int CORE_POOL_SIZE = 2; + + private static final int MAX_POOL_SIZE = 5; + + private static final long KEEP_ALIVE = 30; + + private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + + private static final int MAX_QUEUE_SIZE = 500; + + /** + * The transaction service + */ + private TransactionService transactionService; + + /** + * The authentication component + */ + private AuthenticationComponent authenticationComponent; + + /** + * Default constructor + */ + public AsynchronousActionExecutionQueueImpl() + { + super(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE, TIME_UNIT, new ArrayBlockingQueue(MAX_QUEUE_SIZE, + true)); + } + + /** + * Set the transaction service + * + * @param transactionService + * the transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the authentication component + * + * @param authenticationComponent + * the authentication component + */ + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + /** + * @see org.alfresco.repo.action.AsynchronousActionExecutionQueue#executeAction(org.alfresco.service.cmr.repository.NodeRef, + * org.alfresco.service.cmr.action.Action, boolean) + */ + public void executeAction(RuntimeActionService actionService, Action action, NodeRef actionedUponNodeRef, + boolean checkConditions, Set actionChain) + { + executeAction(actionService, action, actionedUponNodeRef, checkConditions, actionChain, null); + } + + /** + * @see org.alfresco.repo.action.AsynchronousActionExecutionQueue#executeAction(org.alfresco.service.cmr.repository.NodeRef, + * org.alfresco.service.cmr.action.Action, boolean, + * org.alfresco.service.cmr.repository.NodeRef) + */ + public void executeAction(RuntimeActionService actionService, Action action, NodeRef actionedUponNodeRef, + boolean checkConditions, Set actionChain, NodeRef actionExecutionHistoryNodeRef) + { + execute(new ActionExecutionWrapper(actionService, transactionService, authenticationComponent, action, + actionedUponNodeRef, checkConditions, actionExecutionHistoryNodeRef, actionChain)); + } + + /** + * @see java.util.concurrent.ThreadPoolExecutor#beforeExecute(java.lang.Thread, + * java.lang.Runnable) + */ + @Override + protected void beforeExecute(Thread thread, Runnable runnable) + { + super.beforeExecute(thread, runnable); + } + + /** + * @see java.util.concurrent.ThreadPoolExecutor#afterExecute(java.lang.Runnable, + * java.lang.Throwable) + */ + @Override + protected void afterExecute(Runnable thread, Throwable runnable) + { + super.afterExecute(thread, runnable); + } + + /** + * Runnable class to wrap the execution of the action. + */ + private class ActionExecutionWrapper implements Runnable + { + /** + * Runtime action service + */ + private RuntimeActionService actionService; + + /** + * The transaction service + */ + private TransactionService transactionService; + + /** + * The authentication component + */ + private AuthenticationComponent authenticationComponent; + + /** + * The action + */ + private Action action; + + /** + * The actioned upon node reference + */ + private NodeRef actionedUponNodeRef; + + /** + * The check conditions value + */ + private boolean checkConditions; + + /** + * The action execution history node reference + */ + private NodeRef actionExecutionHistoryNodeRef; + + /** + * The action chain + */ + private Set actionChain; + + /** + * Constructor + * + * @param actionService + * @param transactionService + * @param authenticationComponent + * @param action + * @param actionedUponNodeRef + * @param checkConditions + * @param actionExecutionHistoryNodeRef + */ + public ActionExecutionWrapper(RuntimeActionService actionService, TransactionService transactionService, + AuthenticationComponent authenticationComponent, Action action, NodeRef actionedUponNodeRef, + boolean checkConditions, NodeRef actionExecutionHistoryNodeRef, Set actionChain) + { + this.actionService = actionService; + this.transactionService = transactionService; + this.authenticationComponent = authenticationComponent; + this.actionedUponNodeRef = actionedUponNodeRef; + this.action = action; + this.checkConditions = checkConditions; + this.actionExecutionHistoryNodeRef = actionExecutionHistoryNodeRef; + this.actionChain = actionChain; + } + + /** + * Get the action + * + * @return the action + */ + public Action getAction() + { + return this.action; + } + + /** + * Get the actioned upon node reference + * + * @return the actioned upon node reference + */ + public NodeRef getActionedUponNodeRef() + { + return this.actionedUponNodeRef; + } + + /** + * Get the check conditions value + * + * @return the check conditions value + */ + public boolean getCheckCondtions() + { + return this.checkConditions; + } + + /** + * Get the action execution history node reference + * + * @return the action execution history node reference + */ + public NodeRef getActionExecutionHistoryNodeRef() + { + return this.actionExecutionHistoryNodeRef; + } + + /** + * Get the action chain + * + * @return the action chain + */ + public Set getActionChain() + { + return actionChain; + } + + /** + * Executes the action via the action runtime service + * + * @see java.lang.Runnable#run() + */ + @SuppressWarnings("unchecked") + public void run() + { + try + { + + // For now run all actions in the background as the system user + ActionExecutionWrapper.this.authenticationComponent + .setCurrentUser(ActionExecutionWrapper.this.authenticationComponent.getSystemUserName()); + try + { + TransactionUtil.executeInNonPropagatingUserTransaction(this.transactionService, + new TransactionUtil.TransactionWork() + { + public Object doWork() + { + + ActionExecutionWrapper.this.actionService.executeActionImpl( + ActionExecutionWrapper.this.action, + ActionExecutionWrapper.this.actionedUponNodeRef, + ActionExecutionWrapper.this.checkConditions, true, + ActionExecutionWrapper.this.actionChain); + + return null; + } + }); + } + finally + { + ActionExecutionWrapper.this.authenticationComponent.clearCurrentSecurityContext(); + } + } + catch (Throwable exception) + { + exception.printStackTrace(); + } + } + } +} diff --git a/source/java/org/alfresco/repo/action/BaseParameterizedItemDefinitionImplTest.java b/source/java/org/alfresco/repo/action/BaseParameterizedItemDefinitionImplTest.java new file mode 100644 index 0000000000..8b77bff4f5 --- /dev/null +++ b/source/java/org/alfresco/repo/action/BaseParameterizedItemDefinitionImplTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.namespace.QName; + +/** + * @author Roy Wetherall + */ +public abstract class BaseParameterizedItemDefinitionImplTest extends TestCase +{ + protected static final String NAME = "name"; + protected static final String TITLE = "title"; + protected static final String DESCRIPTION = "description"; + protected List paramDefs = new ArrayList(); + protected List duplicateParamDefs = new ArrayList(); + + private static final String PARAM1_DISPLAYNAME = "param1-displayname"; + private static final String PARAM1_NAME = "param1-name"; + private static final QName PARAM1_TYPE = DataTypeDefinition.TEXT; + private static final QName PARAM2_TYPE = DataTypeDefinition.TEXT; + private static final String PARAM2_DISPLAYNAME = "param2-displaname"; + private static final String PARAM2_NAME = "param2-name"; + + @Override + protected void setUp() throws Exception + { + // Create param def lists + this.paramDefs.add(new ParameterDefinitionImpl(PARAM1_NAME, PARAM1_TYPE, false, PARAM1_DISPLAYNAME)); + this.paramDefs.add(new ParameterDefinitionImpl(PARAM2_NAME, PARAM2_TYPE, false, PARAM2_DISPLAYNAME)); + this.duplicateParamDefs.add(new ParameterDefinitionImpl(PARAM1_NAME, PARAM1_TYPE, false, PARAM1_DISPLAYNAME)); + this.duplicateParamDefs.add(new ParameterDefinitionImpl(PARAM1_NAME, PARAM1_TYPE, false, PARAM1_DISPLAYNAME)); + } + + public void testConstructor() + { + create(); + } + + protected abstract ParameterizedItemDefinitionImpl create(); + + public void testGetName() + { + ParameterizedItemDefinitionImpl temp = create(); + assertEquals(NAME, temp.getName()); + } + + public void testGetParameterDefintions() + { + ParameterizedItemDefinitionImpl temp = create(); + List params = temp.getParameterDefinitions(); + assertNotNull(params); + assertEquals(2, params.size()); + int i = 0; + for (ParameterDefinition definition : params) + { + if (i == 0) + { + assertEquals(PARAM1_NAME, definition.getName()); + assertEquals(PARAM1_TYPE, definition.getType()); + assertEquals(PARAM1_DISPLAYNAME, definition.getDisplayLabel()); + } + else + { + assertEquals(PARAM2_NAME, definition.getName()); + assertEquals(PARAM2_TYPE, definition.getType()); + assertEquals(PARAM2_DISPLAYNAME, definition.getDisplayLabel()); + } + i++; + } + } + + public void testGetParameterDefinition() + { + ParameterizedItemDefinitionImpl temp = create(); + ParameterDefinition definition = temp.getParameterDefintion(PARAM1_NAME); + assertNotNull(definition); + assertEquals(PARAM1_NAME, definition.getName()); + assertEquals(PARAM1_TYPE, definition.getType()); + assertEquals(PARAM1_DISPLAYNAME, definition.getDisplayLabel()); + + ParameterDefinition nullDef = temp.getParameterDefintion("bobbins"); + assertNull(nullDef); + } +} diff --git a/source/java/org/alfresco/repo/action/BaseParameterizedItemImplTest.java b/source/java/org/alfresco/repo/action/BaseParameterizedItemImplTest.java new file mode 100644 index 0000000000..5841233a3e --- /dev/null +++ b/source/java/org/alfresco/repo/action/BaseParameterizedItemImplTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; + +/** + * @author Roy Wetherall + */ +public abstract class BaseParameterizedItemImplTest extends TestCase +{ + protected List paramDefs = new ArrayList(); + protected Map paramValues = new HashMap(); + + protected static final String ID = "id"; + protected static final String NAME = "name"; + protected static final String TITLE = "title"; + protected static final String DESCRIPTION = "description"; + + private static final String PARAM_1 = "param1"; + private static final String VALUE_1 = "value1"; + private static final String PARAM_2 = "param2"; + private static final String VALUE_2 = "value2"; + private static final String PARAM_DISPLAYLABEL = "displayLabel"; + + @Override + protected void setUp() throws Exception + { + // Create param defs + paramDefs.add(new ParameterDefinitionImpl(PARAM_1, DataTypeDefinition.TEXT, false, PARAM_DISPLAYLABEL)); + paramDefs.add(new ParameterDefinitionImpl(PARAM_2, DataTypeDefinition.TEXT, false, PARAM_DISPLAYLABEL)); + + // Create param values + paramValues.put(PARAM_1, VALUE_1); + paramValues.put(PARAM_2, VALUE_2); + } + + public void testConstructor() + { + create(); + } + + protected abstract ParameterizedItemImpl create(); + + public void testGetParameterValues() + { + ParameterizedItemImpl temp = create(); + Map tempParamValues = temp.getParameterValues(); + assertNotNull(tempParamValues); + assertEquals(2, tempParamValues.size()); + for (Map.Entry entry : tempParamValues.entrySet()) + { + if (entry.getKey() == PARAM_1) + { + assertEquals(VALUE_1, entry.getValue()); + } + else if (entry.getKey() == PARAM_2) + { + assertEquals(VALUE_2, entry.getValue()); + } + else + { + fail("There is an unexpected entry here."); + } + } + } + + public void testGetParameterValue() + { + ParameterizedItemImpl temp = create(); + assertNull(temp.getParameterValue("bobbins")); + assertEquals(VALUE_1, temp.getParameterValue(PARAM_1)); + } + + public void testSetParameterValue() + { + ParameterizedItemImpl temp = create(); + temp.setParameterValue("bobbins", "value"); + assertEquals("value", temp.getParameterValue("bobbins")); + } + + public void testGetId() + { + ParameterizedItemImpl temp = create(); + assertEquals(ID, temp.getId()); + } +} diff --git a/source/java/org/alfresco/repo/action/CommonResourceAbstractBase.java b/source/java/org/alfresco/repo/action/CommonResourceAbstractBase.java new file mode 100644 index 0000000000..99726ab2a3 --- /dev/null +++ b/source/java/org/alfresco/repo/action/CommonResourceAbstractBase.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import org.springframework.beans.factory.BeanNameAware; + +// TODO this is no longer required + +/** + * Common resouce abstract base class. + * + * @author Roy Wetherall + */ +public abstract class CommonResourceAbstractBase implements BeanNameAware +{ + /** + * The bean name + */ + protected String name; + + /** + * Set the bean name + * + * @param name + * the bean name + */ + public void setBeanName(String name) + { + this.name = name; + } +} diff --git a/source/java/org/alfresco/repo/action/CompositeActionImpl.java b/source/java/org/alfresco/repo/action/CompositeActionImpl.java new file mode 100644 index 0000000000..a761c9a9ad --- /dev/null +++ b/source/java/org/alfresco/repo/action/CompositeActionImpl.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.repo.action.executer.CompositeActionExecuter; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Composite action implementation + * + * @author Roy Wetherall + */ +public class CompositeActionImpl extends ActionImpl implements CompositeAction +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = -5348203599304776812L; + + /** + * The action list + */ + private List actions = new ArrayList(); + + /** + * Constructor + * + * @param id the action id + */ + public CompositeActionImpl(String id, NodeRef owningNodeRef) + { + super(id, CompositeActionExecuter.NAME, owningNodeRef); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#hasActions() + */ + public boolean hasActions() + { + return (this.actions.isEmpty() == false); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#addAction(org.alfresco.service.cmr.action.Action) + */ + public void addAction(Action action) + { + this.actions.add(action); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#addAction(int, org.alfresco.service.cmr.action.Action) + */ + public void addAction(int index, Action action) + { + this.actions.add(index, action); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#setAction(int, org.alfresco.service.cmr.action.Action) + */ + public void setAction(int index, Action action) + { + this.actions.set(index, action); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#indexOfAction(org.alfresco.service.cmr.action.Action) + */ + public int indexOfAction(Action action) + { + return this.actions.indexOf(action); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#getActions() + */ + public List getActions() + { + return this.actions; + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#getAction(int) + */ + public Action getAction(int index) + { + return this.actions.get(index); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#removeAction(org.alfresco.service.cmr.action.Action) + */ + public void removeAction(Action action) + { + this.actions.remove(action); + } + + /** + * @see org.alfresco.service.cmr.action.CompositeAction#removeAllActions() + */ + public void removeAllActions() + { + this.actions.clear(); + } + +} diff --git a/source/java/org/alfresco/repo/action/CompositeActionImplTest.java b/source/java/org/alfresco/repo/action/CompositeActionImplTest.java new file mode 100644 index 0000000000..6d044a79e8 --- /dev/null +++ b/source/java/org/alfresco/repo/action/CompositeActionImplTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.List; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.CompositeAction; + +/** + * Composite action test + * + * @author Roy Wetherall + */ +public class CompositeActionImplTest extends ActionImplTest +{ + private static final String ACTION1_ID = "action1Id"; + private static final String ACTION2_ID = "action2Id"; + private static final String ACTION3_ID = "action3Id"; + private static final String ACTION1_NAME = "actionName1"; + private static final String ACTION2_NAME = "actionName1"; + private static final String ACTION3_NAME = "actionName3"; + + public void testActions() + { + Action action1 = new ActionImpl(ACTION1_ID, ACTION1_NAME, null); + Action action2 = new ActionImpl(ACTION2_ID, ACTION2_NAME, null); + Action action3 = new ActionImpl(ACTION3_ID, ACTION3_NAME, null); + + CompositeAction compositeAction = new CompositeActionImpl(ID, null); + + // Check has no action + assertFalse(compositeAction.hasActions()); + List noActions = compositeAction.getActions(); + assertNotNull(noActions); + assertEquals(0, noActions.size()); + + // Add actions + compositeAction.addAction(action1); + compositeAction.addAction(action2); + compositeAction.addAction(action3); + + // Check that the actions that are there and in the correct order + assertTrue(compositeAction.hasActions()); + List actions = compositeAction.getActions(); + assertNotNull(actions); + assertEquals(3, actions.size()); + int counter = 0; + for (Action action : actions) + { + if (counter == 0) + { + assertEquals(action1, action); + } + else if (counter == 1) + { + assertEquals(action2, action); + } + else if (counter == 2) + { + assertEquals(action3, action); + } + counter+=1; + } + assertEquals(action1, compositeAction.getAction(0)); + assertEquals(action2, compositeAction.getAction(1)); + assertEquals(action3, compositeAction.getAction(2)); + + // Check remove + compositeAction.removeAction(action3); + assertEquals(2, compositeAction.getActions().size()); + + // Check set + compositeAction.setAction(1, action3); + assertEquals(action1, compositeAction.getAction(0)); + assertEquals(action3, compositeAction.getAction(1)); + + // Check index of + assertEquals(0, compositeAction.indexOfAction(action1)); + assertEquals(1, compositeAction.indexOfAction(action3)); + + // Test insert + compositeAction.addAction(1, action2); + assertEquals(3, compositeAction.getActions().size()); + assertEquals(action1, compositeAction.getAction(0)); + assertEquals(action2, compositeAction.getAction(1)); + assertEquals(action3, compositeAction.getAction(2)); + + // Check remote all + compositeAction.removeAllActions(); + assertFalse(compositeAction.hasActions()); + assertEquals(0, compositeAction.getActions().size()); + } +} diff --git a/source/java/org/alfresco/repo/action/ParameterDefinitionImpl.java b/source/java/org/alfresco/repo/action/ParameterDefinitionImpl.java new file mode 100644 index 0000000000..99e0f57556 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ParameterDefinitionImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; + +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.namespace.QName; + +/** + * Parameter definition implementation class. + * + * @author Roy Wetherall + */ +public class ParameterDefinitionImpl implements ParameterDefinition, Serializable +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3976741384558751799L; + + /** + * The name of the parameter + */ + private String name; + + /** + * The type of the parameter + */ + private QName type; + + /** + * The display label + */ + private String displayLabel; + + /** + * Indicates whether it is mandatory for the parameter to be set + */ + private boolean isMandatory = false; + + /** + * Constructor + * + * @param name the name of the parameter + * @param type the type of the parameter + * @param displayLabel the display label + */ + public ParameterDefinitionImpl( + String name, + QName type, + boolean isMandatory, + String displayLabel) + { + this.name = name; + this.type = type; + this.displayLabel = displayLabel; + this.isMandatory = isMandatory; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterDefinition#getName() + */ + public String getName() + { + return this.name; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterDefinition#getType() + */ + public QName getType() + { + return this.type; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterDefinition#isMandatory() + */ + public boolean isMandatory() + { + return this.isMandatory; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterDefinition#getDisplayLabel() + */ + public String getDisplayLabel() + { + return this.displayLabel; + } +} diff --git a/source/java/org/alfresco/repo/action/ParameterDefinitionImplTest.java b/source/java/org/alfresco/repo/action/ParameterDefinitionImplTest.java new file mode 100644 index 0000000000..0651d57707 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ParameterDefinitionImplTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; + +/** + * Parameter definition implementation unit test. + * + * @author Roy Wetherall + */ +public class ParameterDefinitionImplTest extends TestCase +{ + private static final String NAME = "param-name"; + private static final String DISPLAY_LABEL = "The display label."; + + public void testConstructor() + { + create(); + } + + private ParameterDefinitionImpl create() + { + ParameterDefinitionImpl paramDef = new ParameterDefinitionImpl( + NAME, + DataTypeDefinition.TEXT, + true, + DISPLAY_LABEL); + assertNotNull(paramDef); + return paramDef; + } + + public void testGetName() + { + ParameterDefinitionImpl temp = create(); + assertEquals(NAME, temp.getName()); + } + + public void testGetClass() + { + ParameterDefinitionImpl temp = create(); + assertEquals(DataTypeDefinition.TEXT, temp.getType()); + } + + public void testIsMandatory() + { + ParameterDefinitionImpl temp = create(); + assertTrue(temp.isMandatory()); + } + + public void testGetDisplayLabel() + { + ParameterDefinitionImpl temp = create(); + assertEquals(DISPLAY_LABEL, temp.getDisplayLabel()); + } +} diff --git a/source/java/org/alfresco/repo/action/ParameterizedItemAbstractBase.java b/source/java/org/alfresco/repo/action/ParameterizedItemAbstractBase.java new file mode 100644 index 0000000000..a1bd4f882f --- /dev/null +++ b/source/java/org/alfresco/repo/action/ParameterizedItemAbstractBase.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.action.ParameterizedItem; +import org.alfresco.service.cmr.action.ParameterizedItemDefinition; +import org.alfresco.service.cmr.rule.RuleServiceException; + +/** + * Rule item abstract base. + *

    + * Helper base class used by the action exector and condition evaluator implementations. + * + * @author Roy Wetherall + */ +public abstract class ParameterizedItemAbstractBase extends CommonResourceAbstractBase +{ + /** + * Error messages + */ + private static final String ERR_MAND_PROP = "A value for the mandatory parameter {0} has not been set on the rule item {1}"; + + /** + * Look-up constants + */ + private static final String TITLE = "title"; + private static final String DESCRIPTION = "description"; + private static final String DISPLAY_LABEL = "display-label"; + + /** + * Action service + */ + protected RuntimeActionService runtimeActionService; + + /** + * @return Return a short title and description string + */ + public String toString() + { + StringBuilder sb = new StringBuilder(60); + sb.append("ParameterizedItem") + .append("[ title='").append(getTitleKey()).append("'") + .append(", description='").append(getDescriptionKey()).append("'") + .append("]"); + return sb.toString(); + } + + /** + * Gets a list containing the parameter definitions for this rule item. + * + * @return the list of parameter definitions + */ + protected List getParameterDefintions() + { + List result = new ArrayList(); + addParameterDefintions(result); + return result; + } + + /** + * Adds the parameter definitions to the list + * + * @param paramList the parameter definitions list + */ + protected abstract void addParameterDefintions(List paramList); + + /** + * Sets the action service + * + * @param actionRegistration the action service + */ + public void setRuntimeActionService(RuntimeActionService runtimeActionService) + { + this.runtimeActionService = runtimeActionService; + } + + /** + * Gets the title I18N key + * + * @return the title key + */ + protected String getTitleKey() + { + return this.name + "." + TITLE; + } + + /** + * Gets the description I18N key + * + * @return the description key + */ + protected String getDescriptionKey() + { + return this.name + "." + DESCRIPTION; + } + + /** + * Indicates whether adhoc property definitions are allowed or not + * + * @return true if they are, by default false + */ + protected boolean getAdhocPropertiesAllowed() + { + // By default adhoc properties are not allowed + return false; + } + + /** + * Gets the parameter definition display label from the properties file. + * + * @param paramName the name of the parameter + * @return the diaplay label of the parameter + */ + protected String getParamDisplayLabel(String paramName) + { + return I18NUtil.getMessage(this.name + "." + paramName + "." + DISPLAY_LABEL); + } + + /** + * Checked whether all the mandatory parameters for the rule item have been assigned. + * + * @param ruleItem the rule item + * @param ruleItemDefinition the rule item definition + */ + protected void checkMandatoryProperties(ParameterizedItem ruleItem, ParameterizedItemDefinition ruleItemDefinition) + { + List definitions = ruleItemDefinition.getParameterDefinitions(); + for (ParameterDefinition definition : definitions) + { + if (definition.isMandatory() == true) + { + // Check that a value has been set for the mandatory parameter + if (ruleItem.getParameterValue(definition.getName()) == null) + { + // Error since a mandatory parameter has a null value + throw new RuleServiceException( + MessageFormat.format(ERR_MAND_PROP, new Object[]{definition.getName(), ruleItemDefinition.getName()})); + } + } + } + + } +} diff --git a/source/java/org/alfresco/repo/action/ParameterizedItemDefinitionImpl.java b/source/java/org/alfresco/repo/action/ParameterizedItemDefinitionImpl.java new file mode 100644 index 0000000000..7cf778f5fe --- /dev/null +++ b/source/java/org/alfresco/repo/action/ParameterizedItemDefinitionImpl.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.action.ParameterizedItemDefinition; +import org.alfresco.service.cmr.rule.RuleServiceException; + +/** + * Rule item implementation class + * + * @author Roy Wetherall + */ +public abstract class ParameterizedItemDefinitionImpl implements ParameterizedItemDefinition, Serializable +{ + /** + * The name of the rule item + */ + private String name; + + /** + * The title I18N key + */ + private String titleKey; + + /** + * The description I18N key + */ + private String descriptionKey; + + /** + * Indicates whether adHocProperties are allowed + */ + private boolean adhocPropertiesAllowed = false; + + /** + * The list of parameters associated with the rule item + */ + private List parameterDefinitions = new ArrayList(); + + /** + * A map of the parameter definitions by name + */ + private Map paramDefinitionsByName; + + /** + * Error messages + */ + private static final String ERR_NAME_DUPLICATION = "The names " + + "given to parameter definitions must be unique within the " + + "scope of the rule item definition."; + + /** + * Constructor + * + * @param name the name + */ + public ParameterizedItemDefinitionImpl(String name) + { + this.name = name; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItemDefinition#getName() + */ + public String getName() + { + return this.name; + } + + /** + * Set the title of the rule item + * + * @param title the title + */ + public void setTitleKey(String title) + { + this.titleKey = title; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItemDefinition#getTitle() + */ + public String getTitle() + { + return I18NUtil.getMessage(this.titleKey); + } + + /** + * Set the description I18N key + * + * @param descriptionKey the description key + */ + public void setDescriptionKey(String descriptionKey) + { + this.descriptionKey = descriptionKey; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItemDefinition#getDescription() + */ + public String getDescription() + { + return I18NUtil.getMessage(this.descriptionKey); + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItemDefinition#getAdhocPropertiesAllowed() + */ + public boolean getAdhocPropertiesAllowed() + { + return this.adhocPropertiesAllowed; + } + + /** + * Set whether adhoc properties are allowed + * + * @param adhocPropertiesAllowed true is adhoc properties are allowed, false otherwise + */ + public void setAdhocPropertiesAllowed(boolean adhocPropertiesAllowed) + { + this.adhocPropertiesAllowed = adhocPropertiesAllowed; + } + + /** + * Set the parameter definitions for the rule item + * + * @param parameterDefinitions the parameter definitions + */ + public void setParameterDefinitions( + List parameterDefinitions) + { + if (hasDuplicateNames(parameterDefinitions) == true) + { + throw new RuleServiceException(ERR_NAME_DUPLICATION); + } + + this.parameterDefinitions = parameterDefinitions; + + // Create a map of the definitions to use for subsequent calls + this.paramDefinitionsByName = new HashMap(this.parameterDefinitions.size()); + for (ParameterDefinition definition : this.parameterDefinitions) + { + this.paramDefinitionsByName.put(definition.getName(), definition); + } + } + + /** + * Determines whether the list of parameter defintions contains duplicate + * names of not. + * + * @param parameterDefinitions a list of parmeter definitions + * @return true if there are name duplications, false + * otherwise + */ + private boolean hasDuplicateNames(List parameterDefinitions) + { + boolean result = false; + if (parameterDefinitions != null) + { + HashSet temp = new HashSet(parameterDefinitions.size()); + for (ParameterDefinition definition : parameterDefinitions) + { + temp.add(definition.getName()); + } + result = (parameterDefinitions.size() != temp.size()); + } + return result; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItemDefinition#getParameterDefinitions() + */ + public List getParameterDefinitions() + { + return this.parameterDefinitions; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItemDefinition#getParameterDefintion(java.lang.String) + */ + public ParameterDefinition getParameterDefintion(String name) + { + ParameterDefinition result = null; + if (paramDefinitionsByName != null) + { + result = this.paramDefinitionsByName.get(name); + } + return result; + } +} diff --git a/source/java/org/alfresco/repo/action/ParameterizedItemImpl.java b/source/java/org/alfresco/repo/action/ParameterizedItemImpl.java new file mode 100644 index 0000000000..b0233d30aa --- /dev/null +++ b/source/java/org/alfresco/repo/action/ParameterizedItemImpl.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.action.ParameterizedItem; + +/** + * Rule item instance implementation class. + * + * @author Roy Wetherall + */ +public abstract class ParameterizedItemImpl implements ParameterizedItem, Serializable +{ + /** + * The id + */ + private String id; + + /** + * The parameter values + */ + private Map parameterValues = new HashMap(); + + /** + * Constructor + * + * @param ruleItem the rule item + */ + public ParameterizedItemImpl(String id) + { + this(id, null); + } + + /** + * Constructor + * + * @param ruleItem the rule item + * @param parameterValues the parameter values + */ + public ParameterizedItemImpl(String id, Map parameterValues) + { + // Set the action id + this.id = id; + + if (parameterValues != null) + { + // TODO need to check that the parameter values being set correspond + // correctly to the parameter definions on the rule item defintion + this.parameterValues = parameterValues; + } + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItem#getId() + */ + public String getId() + { + return this.id; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItem#getParameterValues() + */ + public Map getParameterValues() + { + Map result = this.parameterValues; + if (result == null) + { + result = new HashMap(); + } + return result; + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItem#getParameterValue(String) + */ + public Serializable getParameterValue(String name) + { + return this.parameterValues.get(name); + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItem#setParameterValues(java.util.Map) + */ + public void setParameterValues(Map parameterValues) + { + if (parameterValues != null) + { + // TODO need to check that the parameter values being set correspond + // correctly to the parameter definions on the rule item defintion + this.parameterValues = parameterValues; + } + } + + /** + * @see org.alfresco.service.cmr.action.ParameterizedItem#setParameterValue(String, Serializable) + */ + public void setParameterValue(String name, Serializable value) + { + this.parameterValues.put(name, value); + } + + /** + * Hash code implementation + */ + @Override + public int hashCode() + { + return this.id.hashCode(); + } + + /** + * Equals implementation + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof ParameterizedItemImpl) + { + ParameterizedItemImpl that = (ParameterizedItemImpl) obj; + return (this.id.equals(that.id)); + } + else + { + return false; + } + } +} diff --git a/source/java/org/alfresco/repo/action/RuntimeActionService.java b/source/java/org/alfresco/repo/action/RuntimeActionService.java new file mode 100644 index 0000000000..0ba0c5bdac --- /dev/null +++ b/source/java/org/alfresco/repo/action/RuntimeActionService.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action; + +import java.util.List; +import java.util.Set; + +import org.alfresco.repo.action.ActionServiceImpl.PendingAction; +import org.alfresco.repo.action.evaluator.ActionConditionEvaluator; +import org.alfresco.repo.action.executer.ActionExecuter; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public interface RuntimeActionService +{ + AsynchronousActionExecutionQueue getAsynchronousActionExecutionQueue(); + + void registerActionConditionEvaluator(ActionConditionEvaluator actionConditionEvaluator); + + void registerActionExecuter(ActionExecuter actionExecuter); + + void populateCompositeAction(NodeRef compositeNodeRef, CompositeAction compositeAction); + + /** + * Save action, used internally to store the details of an action on the aciton node. + * + * @param actionNodeRef the action node reference + * @param action the action + */ + void saveActionImpl(NodeRef owningNodeRef, NodeRef actionNodeRef, Action action); + + /** + * + * @param action + * @param actionedUponNodeRef + * @param checkConditions + */ + public void executeActionImpl( + Action action, + NodeRef actionedUponNodeRef, + boolean checkConditions, + boolean executedAsynchronously, + Set actionChain); + + public void directActionExecution(Action action, NodeRef actionedUponNodeRef); + + public List getPostTransactionPendingActions(); +} diff --git a/source/java/org/alfresco/repo/action/actionModel.xml b/source/java/org/alfresco/repo/action/actionModel.xml new file mode 100644 index 0000000000..9cbdae34fb --- /dev/null +++ b/source/java/org/alfresco/repo/action/actionModel.xml @@ -0,0 +1,199 @@ + + + Alfresco Action Model + Alfresco + 2005-08-16 + 0.1 + + + + + + + + + + + + + + + + + + Action Base Type + cm:cmobject + + + d:text + true + + + + + + act:actionparameter + false + true + + + + + + + Action + act:actionbase + + + d:text + false + + + d:text + false + + + d:boolean + true + + + d:text + false + + + d:text + false + + + + + + act:actioncondition + false + true + + + + + act:action + false + false + + + + + + + Action Condition + act:actionbase + + + d:boolean + true + + + + + + Action/Condition Parameter + cm:cmobject + + + d:text + true + + + d:any + true + + + + + + Composite Action + act:action + + + + act:action + true + true + + + + + + + Saved Action Folder + cm:systemfolder + + + + act:action + true + true + + + + + + + + + Action Execution Details + cm:cmobject + + + d:text + false + + + d:text + true + + + d:text + false + + + d:text + false + + + + + + + + + + + Rules + + + + cm:systemfolder + false + false + + + + + + + + Action Execution History + + + + act:actionexecutiondetails + false + true + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluator.java new file mode 100644 index 0000000000..63a5ae79a7 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluator.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionConditionDefinition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Action Condition Evaluator + * + * @author Roy Wetherall + */ +public interface ActionConditionEvaluator +{ + /** + * Get the action condition deinfinition + * + * @return the action condition definition + */ + public ActionConditionDefinition getActionConditionDefintion(); + + /** + * Evaluate the action condition + * + * @param actionCondition the action condition + * @param actionedUponNodeRef the actioned upon node + * @return true if the condition passes, false otherwise + */ + public boolean evaluate( + ActionCondition actionCondition, + NodeRef actionedUponNodeRef); +} diff --git a/source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluatorAbstractBase.java b/source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluatorAbstractBase.java new file mode 100644 index 0000000000..50850de52c --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/ActionConditionEvaluatorAbstractBase.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import org.alfresco.repo.action.ActionConditionDefinitionImpl; +import org.alfresco.repo.action.ParameterizedItemAbstractBase; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionConditionDefinition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Rule condition evaluator abstract base implementation. + * + * @author Roy Wetherall + */ +public abstract class ActionConditionEvaluatorAbstractBase extends ParameterizedItemAbstractBase implements ActionConditionEvaluator +{ + /** + * Indicates whether the condition is public or not + */ + private boolean publicCondition = true; + + /** + * The action condition definition + */ + protected ActionConditionDefinition actionConditionDefinition; + + /** + * Initialise method + */ + public void init() + { + if (this.publicCondition == true) + { + // Call back to the action service to register the condition + this.runtimeActionService.registerActionConditionEvaluator(this); + } + } + + /** + * Set the value that indicates whether a condition is public or not + * + * @param publicCondition true if the condition is public, false otherwise + */ + public void setPublicCondition(boolean publicCondition) + { + this.publicCondition = publicCondition; + } + + /** + * Get the action condition definition. + * + * @return the action condition definition + */ + public ActionConditionDefinition getActionConditionDefintion() + { + if (this.actionConditionDefinition == null) + { + this.actionConditionDefinition = new ActionConditionDefinitionImpl(this.name); + ((ActionConditionDefinitionImpl)this.actionConditionDefinition).setTitleKey(getTitleKey()); + ((ActionConditionDefinitionImpl)this.actionConditionDefinition).setDescriptionKey(getDescriptionKey()); + ((ActionConditionDefinitionImpl)this.actionConditionDefinition).setAdhocPropertiesAllowed(getAdhocPropertiesAllowed()); + ((ActionConditionDefinitionImpl)this.actionConditionDefinition).setConditionEvaluator(this.name); + ((ActionConditionDefinitionImpl)this.actionConditionDefinition).setParameterDefinitions(getParameterDefintions()); + } + return this.actionConditionDefinition; + } + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluator#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluate(ActionCondition actionCondition, NodeRef actionedUponNodeRef) + { + checkMandatoryProperties(actionCondition, getActionConditionDefintion()); + boolean result = evaluateImpl(actionCondition, actionedUponNodeRef); + if (actionCondition.getInvertCondition() == true) + { + result = !result; + } + return result; + } + + /** + * Evaluation implementation + * + * @param actionCondition the action condition + * @param actionedUponNodeRef the actioned upon node reference + * @return the result of the condition evaluation + */ + protected abstract boolean evaluateImpl(ActionCondition actionCondition, NodeRef actionedUponNodeRef); +} diff --git a/source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluator.java new file mode 100644 index 0000000000..9afded0705 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluator.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.compare.ContentPropertyName; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Compare mime type evaluator + * + * @author Roy Wetherall + */ +public class CompareMimeTypeEvaluator extends ComparePropertyValueEvaluator +{ + /** + * Evaluator constants + */ + public static final String NAME = "compare-mime-type"; + + /** + * + */ + private static final String ERRID_NOT_A_CONTENT_TYPE = "compare_mime_type_evaluator.not_a_content_type"; + private static final String ERRID_NO_PROPERTY_DEFINTION_FOUND = "compare_mime_type_evaluator.no_property_definition_found"; + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluatorAbstractBase#evaluateImpl(org.alfresco.service.cmr.action.ActionCondition, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateImpl(ActionCondition actionCondition, NodeRef actionedUponNodeRef) + { + QName propertyQName = (QName)actionCondition.getParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY); + if (propertyQName == null) + { + // Default to the standard content property + actionCondition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.PROP_CONTENT); + } + else + { + // Ensure that we are dealing with a content property + QName propertyTypeQName = null; + PropertyDefinition propertyDefintion = this.dictionaryService.getProperty(propertyQName); + if (propertyDefintion != null) + { + propertyTypeQName = propertyDefintion.getDataType().getName(); + if (DataTypeDefinition.CONTENT.equals(propertyTypeQName) == false) + { + throw new ActionServiceException(ERRID_NOT_A_CONTENT_TYPE); + } + } + else + { + throw new ActionServiceException(ERRID_NO_PROPERTY_DEFINTION_FOUND); + } + } + + // Set the content property to be MIMETYPE + actionCondition.setParameterValue(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, ContentPropertyName.MIME_TYPE.toString()); + + return super.evaluateImpl(actionCondition, actionedUponNodeRef); + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + super.addParameterDefintions(paramList); + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluatorTest.java b/source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluatorTest.java new file mode 100644 index 0000000000..1f2ef7ee12 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/CompareMimeTypeEvaluatorTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Compare property value evaluator test + * + * @author Roy Wetherall + */ +public class CompareMimeTypeEvaluatorTest extends BaseSpringTest +{ + private NodeService nodeService; + private ContentService contentService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private CompareMimeTypeEvaluator evaluator; + + /** + * @see org.springframework.test.AbstractTransactionalSpringContextTests#onSetUpInTransaction() + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + this.evaluator = (CompareMimeTypeEvaluator)this.applicationContext.getBean(CompareMimeTypeEvaluator.NAME); + } + + public void testContentPropertyComparisons() + { + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + + // What happens if you do this and the node has no content set yet !! + + // Add some content to the node reference + ContentWriter contentWriter = this.contentService.getWriter(this.nodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setEncoding("UTF-8"); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.putContent("This is some test content."); + + // Test matching the mimetype + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, MimetypeMap.MIMETYPE_HTML); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluator.java new file mode 100644 index 0000000000..f44f2cf82c --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluator.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation; +import org.alfresco.repo.action.evaluator.compare.ContentPropertyName; +import org.alfresco.repo.action.evaluator.compare.PropertyValueComparator; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.namespace.QName; + +/** + * Compare property value evaluator + * + * @author Roy Wetherall + */ +public class ComparePropertyValueEvaluator extends ActionConditionEvaluatorAbstractBase +{ + /** + * Evaluator constants + */ + public final static String NAME = "compare-property-value"; + public final static String PARAM_PROPERTY = "property"; + public final static String PARAM_CONTENT_PROPERTY = "content-property"; + public final static String PARAM_VALUE = "value"; + public final static String PARAM_OPERATION = "operation"; + + /** + * The default property to check if none is specified in the properties + */ + private final static QName DEFAULT_PROPERTY = ContentModel.PROP_NAME; + + /** + * I18N message ID's + */ + private static final String MSGID_INVALID_OPERATION = "compare_property_value_evaluator.invalid_operation"; + private static final String MSGID_NO_CONTENT_PROPERTY = "compare_property_value_evaluator.no_content_property"; + + /** + * Map of comparators used by different property types + */ + private Map comparators = new HashMap(); + + /** + * The node service + */ + protected NodeService nodeService; + + /** + * The content service + */ + protected ContentService contentService; + + /** + * The dictionary service + */ + protected DictionaryService dictionaryService; + + /** + * Set node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the content service + * + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Set the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set the list of property value comparators + * + * @param comparators the list of property value comparators + */ + public void setPropertyValueComparators(List comparators) + { + for (PropertyValueComparator comparator : comparators) + { + comparator.registerComparator(this); + } + } + + /** + * Registers a comparator for a given property data type. + * + * @param dataType property data type + * @param comparator property value comparator + */ + public void registerComparator(QName dataType, PropertyValueComparator comparator) + { + this.comparators.put(dataType, comparator); + } + + /** + * Add paremeter defintions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_PROPERTY, DataTypeDefinition.QNAME, false, getParamDisplayLabel(PARAM_PROPERTY))); + paramList.add(new ParameterDefinitionImpl(PARAM_CONTENT_PROPERTY, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_CONTENT_PROPERTY))); + paramList.add(new ParameterDefinitionImpl(PARAM_VALUE, DataTypeDefinition.ANY, true, getParamDisplayLabel(PARAM_VALUE))); + paramList.add(new ParameterDefinitionImpl(PARAM_OPERATION, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_OPERATION))); + } + + /** + * @see ActionConditionEvaluatorAbstractBase#evaluateImpl(ActionCondition, NodeRef) + */ + public boolean evaluateImpl( + ActionCondition ruleCondition, + NodeRef actionedUponNodeRef) + { + boolean result = false; + + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // Get the name value of the node + QName propertyQName = (QName)ruleCondition.getParameterValue(PARAM_PROPERTY); + if (propertyQName == null) + { + propertyQName = DEFAULT_PROPERTY; + } + + // Get the origional value and the value to match + Serializable propertyValue = this.nodeService.getProperty(actionedUponNodeRef, propertyQName); + Serializable compareValue = ruleCondition.getParameterValue(PARAM_VALUE); + + // Get the operation + ComparePropertyValueOperation operation = null; + String operationString = (String)ruleCondition.getParameterValue(PARAM_OPERATION); + if (operationString != null) + { + operation = ComparePropertyValueOperation.valueOf(operationString); + } + + // Look at the type of the property (assume to be ANY if none found in dicitionary) + QName propertyTypeQName = DataTypeDefinition.ANY; + PropertyDefinition propertyDefintion = this.dictionaryService.getProperty(propertyQName); + if (propertyDefintion != null) + { + propertyTypeQName = propertyDefintion.getDataType().getName(); + } + + // Sort out what to do if the property is a content property + if (DataTypeDefinition.CONTENT.equals(propertyTypeQName) == true) + { + // Get the content property name + ContentPropertyName contentProperty = null; + String contentPropertyString = (String)ruleCondition.getParameterValue(PARAM_CONTENT_PROPERTY); + if (contentPropertyString == null) + { + // Error if no content property has been set + throw new ActionServiceException(MSGID_NO_CONTENT_PROPERTY); + } + else + { + contentProperty = ContentPropertyName.valueOf(contentPropertyString); + } + + // Get the content data + if (propertyValue != null) + { + ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, propertyValue); + switch (contentProperty) + { + case ENCODING: + { + propertyTypeQName = DataTypeDefinition.TEXT; + propertyValue = contentData.getEncoding(); + break; + } + case SIZE: + { + propertyTypeQName = DataTypeDefinition.LONG; + propertyValue = contentData.getSize(); + break; + } + case MIME_TYPE: + { + propertyTypeQName = DataTypeDefinition.TEXT; + propertyValue = contentData.getMimetype(); + break; + } + } + } + } + + if (propertyValue != null) + { + // Try and get a matching comparator + PropertyValueComparator comparator = this.comparators.get(propertyTypeQName); + if (comparator != null) + { + // Call the comparator for the property type + result = comparator.compare(propertyValue, compareValue, operation); + } + else + { + // The default behaviour is to assume the property can only be compared using equals + if (operation != null && operation != ComparePropertyValueOperation.EQUALS) + { + // Error since only the equals operation is valid + throw new ActionServiceException( + MSGID_INVALID_OPERATION, + new Object[]{operation.toString(), propertyTypeQName.toString()}); + } + + // Use equals to compare the values + result = compareValue.equals(propertyValue); + } + } + } + + return result; + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluatorTest.java b/source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluatorTest.java new file mode 100644 index 0000000000..da20a07f4e --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/ComparePropertyValueEvaluatorTest.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation; +import org.alfresco.repo.action.evaluator.compare.ContentPropertyName; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.dictionary.M2Type; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Compare property value evaluator test + * + * @author Roy Wetherall + */ +public class ComparePropertyValueEvaluatorTest extends BaseSpringTest +{ + private static final String TEST_TYPE_NAMESPACE = "testNamespace"; + private static final QName TEST_TYPE_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testType"); + private static final QName PROP_TEXT = QName.createQName(TEST_TYPE_NAMESPACE, "propText"); + private static final QName PROP_INT = QName.createQName(TEST_TYPE_NAMESPACE, "propInt"); + private static final QName PROP_DATETIME = QName.createQName(TEST_TYPE_NAMESPACE, "propDatetime"); + private static final QName PROP_NODEREF = QName.createQName(TEST_TYPE_NAMESPACE, "propNodeRef"); + + private static final String TEXT_VALUE = "myDocument.doc"; + private static final int INT_VALUE = 100; + + private Date beforeDateValue; + private Date dateValue; + private Date afterDateValue; + private NodeRef nodeValue; + + private DictionaryDAO dictionaryDAO; + private NodeService nodeService; + private ContentService contentService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private ComparePropertyValueEvaluator evaluator; + + /** + * Sets the meta model DAO + * + * @param dictionaryDAO the meta model DAO + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + /** + * @see org.springframework.test.AbstractTransactionalSpringContextTests#onSetUpInTransaction() + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + // Need to create model to contain our custom type + createTestModel(); + + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + this.nodeValue = new NodeRef(this.testStoreRef, "1234"); + + this.beforeDateValue = new Date(); + Thread.sleep(2000); + this.dateValue = new Date(); + Thread.sleep(2000); + this.afterDateValue = new Date(); + + Map props = new HashMap(); + props.put(PROP_TEXT, TEXT_VALUE); + props.put(PROP_INT, INT_VALUE); + props.put(PROP_DATETIME, this.dateValue); + props.put(PROP_NODEREF, this.nodeValue); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + TEST_TYPE_QNAME, + props).getChildRef(); + + this.evaluator = (ComparePropertyValueEvaluator)this.applicationContext.getBean(ComparePropertyValueEvaluator.NAME); + } + + /** + * Test numeric comparisions + */ + public void testNumericComparison() + { + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, PROP_INT); + + // Test the default operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, INT_VALUE); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 101); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, INT_VALUE); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 101); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals greater than operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 99); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 101); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals greater than operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN_EQUAL.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 99); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 100); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 101); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals less than operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 101); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 99); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals less than equals operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN_EQUAL.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 101); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 100); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 99); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Ensure other operators are invalid + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.BEGINS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {exception.printStackTrace();}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.ENDS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.CONTAINS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + } + + /** + * Test date comparison + */ + public void testDateComparison() + { + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, PROP_DATETIME); + + // Test the default operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.dateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, new Date()); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test the equals operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.dateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, new Date()); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals greater than operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.beforeDateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.afterDateValue); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals greater than operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN_EQUAL.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.beforeDateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.dateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.afterDateValue); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals less than operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.afterDateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.beforeDateValue); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals less than equals operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN_EQUAL.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.afterDateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.dateValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.beforeDateValue); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Ensure other operators are invalid + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.BEGINS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {exception.printStackTrace();}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.ENDS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.CONTAINS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + } + + /** + * Test text comparison + */ + public void testTextComparison() + { + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, PROP_TEXT); + + // Test default operations implied by presence and position of * + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.doc"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "*.xls"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "my*"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "bad*"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "Document"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "bobbins"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals operator + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, TEXT_VALUE); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "bobbins"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test contains operator + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.CONTAINS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "Document"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "bobbins"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test begins operator + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.BEGINS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "my"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "bobbins"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test ends operator + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.ENDS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "doc"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "bobbins"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Ensure other operators are invalid + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {exception.printStackTrace();}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN_EQUAL.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN_EQUAL.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + } + + /** + * Test some combinations of test file names that had been failing + */ + public void testTempFileNames() + { + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, PROP_TEXT); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "~*.doc"); + this.nodeService.setProperty(this.nodeRef, PROP_TEXT, "~1234.doc"); + + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + } + + /** + * Test comparison of properties that do not have a registered comparitor + */ + public void testOtherComparison() + { + NodeRef badNodeRef = new NodeRef(this.testStoreRef, "badId"); + + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, PROP_NODEREF); + + // Test default operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.nodeValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, badNodeRef); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "this isn't even the correct type!"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test equals operation + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS.toString()); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, this.nodeValue); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, badNodeRef); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Ensure other operators are invalid + + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.BEGINS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) { exception.printStackTrace();}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.ENDS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.CONTAINS.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.GREATER_THAN_EQUAL.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN_EQUAL.toString()); + try { this.evaluator.evaluate(condition, this.nodeRef); fail("An exception should have been raised here."); } catch (ActionServiceException exception) {}; + + } + + public void testContentPropertyComparisons() + { + ActionConditionImpl condition = new ActionConditionImpl(GUID.generate(), ComparePropertyValueEvaluator.NAME); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.PROP_CONTENT); + + // What happens if you do this and the node has no content set yet !! + + // Add some content to the node reference + ContentWriter contentWriter = this.contentService.getWriter(this.nodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setEncoding("UTF-8"); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.putContent("This is some test content."); + + // Test matching the mimetype + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, ContentPropertyName.MIME_TYPE.toString()); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, MimetypeMap.MIMETYPE_HTML); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test matching the encoding + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, ContentPropertyName.ENCODING.toString()); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "UTF-8"); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, "UTF-16"); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + // Test comparision to the size of the content + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, ContentPropertyName.SIZE.toString()); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.LESS_THAN.toString()); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 50); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + condition.setParameterValue(ComparePropertyValueEvaluator.PARAM_VALUE, 2); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + + + } + + private void createTestModel() + { + M2Model model = M2Model.createModel("test:comparepropertyvalueevaluatortest"); + model.createNamespace(TEST_TYPE_NAMESPACE, "test"); + model.createImport(NamespaceService.DICTIONARY_MODEL_1_0_URI, NamespaceService.DICTIONARY_MODEL_PREFIX); + model.createImport(NamespaceService.SYSTEM_MODEL_1_0_URI, NamespaceService.SYSTEM_MODEL_PREFIX); + model.createImport(NamespaceService.CONTENT_MODEL_1_0_URI, NamespaceService.CONTENT_MODEL_PREFIX); + + M2Type testType = model.createType("test:" + TEST_TYPE_QNAME.getLocalName()); + testType.setParentName("cm:" + ContentModel.TYPE_CONTENT.getLocalName()); + + M2Property prop1 = testType.createProperty("test:" + PROP_TEXT.getLocalName()); + prop1.setMandatory(false); + prop1.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop1.setMultiValued(false); + + M2Property prop2 = testType.createProperty("test:" + PROP_INT.getLocalName()); + prop2.setMandatory(false); + prop2.setType("d:" + DataTypeDefinition.INT.getLocalName()); + prop2.setMultiValued(false); + + M2Property prop3 = testType.createProperty("test:" + PROP_DATETIME.getLocalName()); + prop3.setMandatory(false); + prop3.setType("d:" + DataTypeDefinition.DATETIME.getLocalName()); + prop3.setMultiValued(false); + + M2Property prop4 = testType.createProperty("test:" + PROP_NODEREF.getLocalName()); + prop4.setMandatory(false); + prop4.setType("d:" + DataTypeDefinition.NODE_REF.getLocalName()); + prop4.setMultiValued(false); + + dictionaryDAO.putModel(model); + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluator.java new file mode 100644 index 0000000000..a5e5da26b3 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluator.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Has aspect evaluator + * + * @author Roy Wetherall + */ +public class HasAspectEvaluator extends ActionConditionEvaluatorAbstractBase +{ + /** + * Evaluator constants + */ + public static final String NAME = "has-aspect"; + public static final String PARAM_ASPECT = "aspect"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * Set node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluatorAbstractBase#evaluateImpl(org.alfresco.service.cmr.action.ActionCondition, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateImpl(ActionCondition ruleCondition, NodeRef actionedUponNodeRef) + { + boolean result = false; + + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + if (this.nodeService.hasAspect(actionedUponNodeRef, (QName)ruleCondition.getParameterValue(PARAM_ASPECT)) == true) + { + result = true; + } + } + + return result; + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_ASPECT, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASPECT))); + } + +} diff --git a/source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluatorTest.java b/source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluatorTest.java new file mode 100644 index 0000000000..ee05d86bb3 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/HasAspectEvaluatorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Is sub class evaluator test + * + * @author Roy Wetherall + */ +public class HasAspectEvaluatorTest extends BaseSpringTest +{ + private NodeService nodeService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private HasAspectEvaluator evaluator; + + private final static String ID = GUID.generate(); + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + this.evaluator = (HasAspectEvaluator)this.applicationContext.getBean(HasAspectEvaluator.NAME); + } + + public void testMandatoryParamsMissing() + { + ActionCondition condition = new ActionConditionImpl(ID, HasAspectEvaluator.NAME, null); + + try + { + this.evaluator.evaluate(condition, this.nodeRef); + fail("The fact that a mandatory parameter has not been set should have been detected."); + } + catch (Throwable exception) + { + // Do nothing since this is correct + } + } + + public void testPass() + { + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + ActionCondition condition = new ActionConditionImpl(ID, HasAspectEvaluator.NAME, null); + condition.setParameterValue(HasAspectEvaluator.PARAM_ASPECT, ContentModel.ASPECT_VERSIONABLE); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + } + + public void testFail() + { + ActionCondition condition = new ActionConditionImpl(ID, HasAspectEvaluator.NAME, null); + condition.setParameterValue(HasAspectEvaluator.PARAM_ASPECT, ContentModel.ASPECT_VERSIONABLE); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/HasVersionHistoryEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/HasVersionHistoryEvaluator.java new file mode 100644 index 0000000000..dccdf4a0f4 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/HasVersionHistoryEvaluator.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.version.VersionHistory; +import org.alfresco.service.cmr.version.VersionService; + +/** + * Has version history evaluator + * + * @author Roy Wetherall + */ +public class HasVersionHistoryEvaluator extends ActionConditionEvaluatorAbstractBase +{ + /** + * Evaluator constants + */ + public static final String NAME = "has-version-history"; + + /** + * The node service + */ + private NodeService nodeService; + + private VersionService versionService; + + /** + * Set node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setVersionService(VersionService versionService) + { + this.versionService = versionService; + } + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluatorAbstractBase#evaluateImpl(org.alfresco.service.cmr.action.ActionCondition, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateImpl(ActionCondition ruleCondition, NodeRef actionedUponNodeRef) + { + boolean result = false; + + if (this.nodeService.exists(actionedUponNodeRef) == true && + this.nodeService.hasAspect(actionedUponNodeRef, ContentModel.ASPECT_VERSIONABLE) == true) + { + VersionHistory versionHistory = this.versionService.getVersionHistory(actionedUponNodeRef); + if (versionHistory != null && versionHistory.getAllVersions().size() != 0) + { + result = true; + } + } + + return result; + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + } + +} diff --git a/source/java/org/alfresco/repo/action/evaluator/InCategoryEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/InCategoryEvaluator.java new file mode 100644 index 0000000000..010b87e820 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/InCategoryEvaluator.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.namespace.QName; + +/** + * In category evaluator implementation. + * + * @author Roy Wetherall + */ +public class InCategoryEvaluator extends ActionConditionEvaluatorAbstractBase +{ + /** + * Rule constants + */ + public static final String NAME = "in-category"; + public static final String PARAM_CATEGORY_ASPECT = "category-aspect"; + public static final String PARAM_CATEGORY_VALUE = "category-value"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The dictionary service + */ + private DictionaryService dictionaryService; + + /** + * Sets the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Add the parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_CATEGORY_ASPECT, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_CATEGORY_ASPECT))); + paramList.add(new ParameterDefinitionImpl(PARAM_CATEGORY_VALUE, DataTypeDefinition.NODE_REF, true, getParamDisplayLabel(PARAM_CATEGORY_VALUE))); + } + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluatorAbstractBase#evaluateImpl(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected boolean evaluateImpl( + ActionCondition ruleCondition, + NodeRef actionedUponNodeRef) + { + boolean result = false; + + // Double check that the node still exists + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // Get the rule parameter values + QName categoryAspect = (QName)ruleCondition.getParameterValue(PARAM_CATEGORY_ASPECT); + NodeRef categoryValue = (NodeRef)ruleCondition.getParameterValue(PARAM_CATEGORY_VALUE); + + // Check that the apect is classifiable and is currently applied to the node + if (this.dictionaryService.isSubClass(categoryAspect, ContentModel.ASPECT_CLASSIFIABLE) == true && + this.nodeService.hasAspect(actionedUponNodeRef, categoryAspect) == true) + { + // Get the category property qname + QName categoryProperty = null; + Map propertyDefs = this.dictionaryService.getAspect(categoryAspect).getProperties(); + for (Map.Entry entry : propertyDefs.entrySet()) + { + if (DataTypeDefinition.CATEGORY.equals(entry.getValue().getDataType().getName()) == true) + { + // Found the category property + categoryProperty = entry.getKey(); + break; + } + } + + if (categoryProperty != null) + { + // Check to see if the category value is in the list of currently set category values + Serializable value = this.nodeService.getProperty(actionedUponNodeRef, categoryProperty); + Collection actualCategories = DefaultTypeConverter.INSTANCE.getCollection(NodeRef.class, value); + for (NodeRef nodeRef : actualCategories) + { + if (nodeRef.equals(categoryValue) == true) + { + result = true; + break; + } + } + } + } + + } + + return result; + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluator.java new file mode 100644 index 0000000000..8cf1c6531e --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluator.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * No condition evaluator implmentation. + * + * @author Roy Wetherall + */ +public class IsSubTypeEvaluator extends ActionConditionEvaluatorAbstractBase +{ + /** + * Evaluator constants + */ + public static final String NAME = "is-subtype"; + public static final String PARAM_TYPE = "type"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The dictionary service + */ + private DictionaryService dictionaryService; + + /** + * Set node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluatorAbstractBase#evaluateImpl(org.alfresco.service.cmr.action.ActionCondition, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateImpl(ActionCondition ruleCondition, NodeRef actionedUponNodeRef) + { + boolean result = false; + + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // TODO: Move this type check into its own Class Evaluator + QName nodeType = nodeService.getType(actionedUponNodeRef); + if (dictionaryService.isSubClass(nodeType, (QName)ruleCondition.getParameterValue(PARAM_TYPE))) + { + result = true; + } + } + + return result; + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_TYPE, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_TYPE))); + } + +} diff --git a/source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluatorTest.java b/source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluatorTest.java new file mode 100644 index 0000000000..9643374411 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/IsSubTypeEvaluatorTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Is sub class evaluator test + * + * @author Roy Wetherall + */ +public class IsSubTypeEvaluatorTest extends BaseSpringTest +{ + private NodeService nodeService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private IsSubTypeEvaluator evaluator; + + private final static String ID = GUID.generate(); + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + this.evaluator = (IsSubTypeEvaluator)this.applicationContext.getBean(IsSubTypeEvaluator.NAME); + } + + public void testMandatoryParamsMissing() + { + ActionCondition condition = new ActionConditionImpl(ID, IsSubTypeEvaluator.NAME, null); + + try + { + this.evaluator.evaluate(condition, this.nodeRef); + fail("The fact that a mandatory parameter has not been set should have been detected."); + } + catch (Throwable exception) + { + // Do nothing since this is correct + } + } + + public void testPass() + { + ActionCondition condition = new ActionConditionImpl(ID, IsSubTypeEvaluator.NAME, null); + condition.setParameterValue(IsSubTypeEvaluator.PARAM_TYPE, ContentModel.TYPE_CONTENT); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + condition.setParameterValue(IsSubTypeEvaluator.PARAM_TYPE, ContentModel.TYPE_CMOBJECT); + assertTrue(this.evaluator.evaluate(condition, this.nodeRef)); + } + + public void testFail() + { + ActionCondition condition = new ActionConditionImpl(ID, IsSubTypeEvaluator.NAME, null); + condition.setParameterValue(IsSubTypeEvaluator.PARAM_TYPE, ContentModel.TYPE_FOLDER); + assertFalse(this.evaluator.evaluate(condition, this.nodeRef)); + } +} diff --git a/source/java/org/alfresco/repo/action/evaluator/NoConditionEvaluator.java b/source/java/org/alfresco/repo/action/evaluator/NoConditionEvaluator.java new file mode 100644 index 0000000000..7ff73476b8 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/NoConditionEvaluator.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator; + +import java.util.List; + +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * No condition evaluator implmentation. + * + * @author Roy Wetherall + */ +public class NoConditionEvaluator extends ActionConditionEvaluatorAbstractBase +{ + /** + * Evaluator constants + */ + public static final String NAME = "no-condition"; + + /** + * @see org.alfresco.repo.action.evaluator.ActionConditionEvaluatorAbstractBase#evaluateImpl(org.alfresco.service.cmr.action.ActionCondition, org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean evaluateImpl(ActionCondition ruleCondition, NodeRef actionedUponNodeRef) + { + return true; + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + // No parameters to add + } + +} diff --git a/source/java/org/alfresco/repo/action/evaluator/compare/ComparePropertyValueOperation.java b/source/java/org/alfresco/repo/action/evaluator/compare/ComparePropertyValueOperation.java new file mode 100644 index 0000000000..9e036f3946 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/compare/ComparePropertyValueOperation.java @@ -0,0 +1,22 @@ +package org.alfresco.repo.action.evaluator.compare; + +/** + * ComparePropertyValueOperation enum. + *

    + * Contains the operations that can be used when evaluating whether the value of a property + * matches the value set. + *

    + * Some operations can only be used with specific types. If a mismatch is encountered an error will + * be raised. + */ +public enum ComparePropertyValueOperation +{ + EQUALS, // All property types + CONTAINS, // String properties only + BEGINS, // String properties only + ENDS, // String properties only + GREATER_THAN, // Numeric and date properties only + GREATER_THAN_EQUAL, // Numeric and date properties only + LESS_THAN, // Numeric and date properties only + LESS_THAN_EQUAL // Numeric and date properties only +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/action/evaluator/compare/ContentPropertyName.java b/source/java/org/alfresco/repo/action/evaluator/compare/ContentPropertyName.java new file mode 100644 index 0000000000..b1e933b8f5 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/compare/ContentPropertyName.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator.compare; + +/** + * @author Roy Wetherall + */ +public enum ContentPropertyName +{ + MIME_TYPE, + ENCODING, + SIZE +} diff --git a/source/java/org/alfresco/repo/action/evaluator/compare/DatePropertyValueComparator.java b/source/java/org/alfresco/repo/action/evaluator/compare/DatePropertyValueComparator.java new file mode 100644 index 0000000000..d84af4e795 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/compare/DatePropertyValueComparator.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator.compare; + +import java.io.Serializable; +import java.util.Date; + +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; + +/** + * Date property value comparator + * + * @author Roy Wetherall + */ +public class DatePropertyValueComparator implements PropertyValueComparator +{ + /** + * I18N message ids + */ + private static final String MSGID_INVALID_OPERATION = "date_property_value_comparator.invalid_operation"; + + /** + * @see org.alfresco.repo.action.evaluator.compare.PropertyValueComparator#compare(java.io.Serializable, java.io.Serializable, org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation) + */ + public boolean compare(Serializable propertyValue, + Serializable compareValue, ComparePropertyValueOperation operation) + { + boolean result = false; + + if (operation == null) + { + operation = ComparePropertyValueOperation.EQUALS; + } + + Date propertyDate = (Date)propertyValue; + Date compareDate = (Date)compareValue; + + switch (operation) + { + case EQUALS: + { + result = propertyDate.equals(compareDate); + break; + } + case LESS_THAN: + { + result = propertyDate.before(compareDate); + break; + } + case LESS_THAN_EQUAL: + { + result = (propertyDate.equals(compareDate) || propertyDate.before(compareDate)); + break; + } + case GREATER_THAN: + { + result = propertyDate.after(compareDate); + break; + } + case GREATER_THAN_EQUAL: + { + result = (propertyDate.equals(compareDate) || propertyDate.after(compareDate)); + break; + } + default: + { + // Raise an invalid operation exception + throw new ActionServiceException( + MSGID_INVALID_OPERATION, + new Object[]{operation.toString()}); + } + } + + return result; + } + + /** + * @see org.alfresco.repo.action.evaluator.compare.PropertyValueComparator#registerComparator(org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator) + */ + public void registerComparator(ComparePropertyValueEvaluator evaluator) + { + evaluator.registerComparator(DataTypeDefinition.DATE, this); + evaluator.registerComparator(DataTypeDefinition.DATETIME, this); + } + +} diff --git a/source/java/org/alfresco/repo/action/evaluator/compare/NumericPropertyValueComparator.java b/source/java/org/alfresco/repo/action/evaluator/compare/NumericPropertyValueComparator.java new file mode 100644 index 0000000000..275e7c8fb0 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/compare/NumericPropertyValueComparator.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator.compare; + +import java.io.Serializable; + +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; + +/** + * Numeric property value comparator. + * + * @author Roy Wetherall + */ +public class NumericPropertyValueComparator implements PropertyValueComparator +{ + /** + * I18N message ids + */ + private static final String MSGID_INVALID_OPERATION = "numeric_property_value_comparator.invalid_operation"; + + /** + * @see org.alfresco.repo.action.evaluator.compare.PropertyValueComparator#compare(java.io.Serializable, java.io.Serializable, org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation) + */ + public boolean compare( + Serializable propertyValue, + Serializable compareValue, + ComparePropertyValueOperation operation) + { + boolean result = false; + if (operation == null) + { + operation = ComparePropertyValueOperation.EQUALS; + } + + // TODO need to check that doing this potential conversion does not cause a problem + double property = ((Number)propertyValue).doubleValue(); + double compare = ((Number)compareValue).doubleValue(); + + switch (operation) + { + case EQUALS: + { + result = (property == compare); + break; + } + case GREATER_THAN: + { + result = (property > compare); + break; + } + case GREATER_THAN_EQUAL: + { + result = (property >= compare); + break; + } + case LESS_THAN: + { + result = (property < compare); + break; + } + case LESS_THAN_EQUAL: + { + result = (property <= compare); + break; + } + default: + { + // Raise an invalid operation exception + throw new ActionServiceException( + MSGID_INVALID_OPERATION, + new Object[]{operation.toString()}); + } + } + + return result; + } + + /** + * @see org.alfresco.repo.action.evaluator.compare.PropertyValueComparator#registerComparator(org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator) + */ + public void registerComparator(ComparePropertyValueEvaluator evaluator) + { + evaluator.registerComparator(DataTypeDefinition.DOUBLE, this); + evaluator.registerComparator(DataTypeDefinition.FLOAT, this); + evaluator.registerComparator(DataTypeDefinition.INT, this); + evaluator.registerComparator(DataTypeDefinition.LONG, this); + + } + +} diff --git a/source/java/org/alfresco/repo/action/evaluator/compare/PropertyValueComparator.java b/source/java/org/alfresco/repo/action/evaluator/compare/PropertyValueComparator.java new file mode 100644 index 0000000000..492c41d147 --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/compare/PropertyValueComparator.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator.compare; + +import java.io.Serializable; + +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; + +/** + * Property value comparator interface + * + * @author Roy Wetherall + */ +public interface PropertyValueComparator +{ + /** + * Callback method to register this comparator with the evaluator. + * + * @param evaluator the compare property value evaluator + */ + void registerComparator(ComparePropertyValueEvaluator evaluator); + + /** + * Compares the value of a property with the compare value, using the operator passed. + * + * @param propertyValue the property value + * @param compareValue the compare value + * @param operation the operation used to compare the two values + * @return the result of the comparision, true if successful false otherwise + */ + boolean compare( + Serializable propertyValue, + Serializable compareValue, + ComparePropertyValueOperation operation); +} diff --git a/source/java/org/alfresco/repo/action/evaluator/compare/TextPropertyValueComparator.java b/source/java/org/alfresco/repo/action/evaluator/compare/TextPropertyValueComparator.java new file mode 100644 index 0000000000..4e3bd2051c --- /dev/null +++ b/source/java/org/alfresco/repo/action/evaluator/compare/TextPropertyValueComparator.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.evaluator.compare; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; + +/** + * Test property value comparator + * + * @author Roy Wetherall + */ +public class TextPropertyValueComparator implements PropertyValueComparator +{ + /** + * I18N message ids + */ + private static final String MSGID_INVALID_OPERATION = "text_property_value_comparator.invalid_operation"; + + /** + * Special star string + */ + private static final String STAR = "*"; + + /** + * @see org.alfresco.repo.action.evaluator.compare.PropertyValueComparator#compare(java.io.Serializable, java.io.Serializable, org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation) + */ + public boolean compare( + Serializable propertyValue, + Serializable compareValue, + ComparePropertyValueOperation operation) + { + String compareText = (String)compareValue; + + boolean result = false; + if (operation == null) + { + // Check for a trailing or leading star since it implies special behaviour when no default operation is specified + if (compareText.startsWith(STAR) == true) + { + // Remove the star and set the operation to endsWith + operation = ComparePropertyValueOperation.ENDS; + compareText = compareText.substring(1); + } + else if (compareText.endsWith(STAR) == true) + { + // Remove the star and set the operation to startsWith + operation = ComparePropertyValueOperation.BEGINS; + compareText = compareText.substring(0, (compareText.length()-2)); + } + else + { + operation = ComparePropertyValueOperation.CONTAINS; + } + } + + // Build the reg ex + String regEx = buildRegEx(compareText, operation); + + // Do the match + if (propertyValue != null) + { + result = ((String)propertyValue).toLowerCase().matches(regEx); + } + + return result; + } + + /** + * Builds the regular expressin that it used to make the match + * + * @param matchText the raw text to be matched + * @param operation the operation + * @return the regular expression string + */ + private String buildRegEx(String matchText, ComparePropertyValueOperation operation) + { + String result = escapeText(matchText.toLowerCase()); + switch (operation) + { + case CONTAINS: + result = "^.*" + result + ".*$"; + break; + case BEGINS: + result = "^" + result + ".*$"; + break; + case ENDS: + result = "^.*" + result + "$"; + break; + case EQUALS: + break; + default: + // Raise an invalid operation exception + throw new ActionServiceException( + MSGID_INVALID_OPERATION, + new Object[]{operation.toString()}); + } + return result; + } + + /** + * Escapes the text before it is turned into a regualr expression + * + * @param matchText the raw text + * @return the escaped text + */ + private String escapeText(String matchText) + { + StringBuilder builder = new StringBuilder(matchText.length()); + for (char charValue : matchText.toCharArray()) + { + if (charValue == '*') + { + builder.append("."); + } + else if (getEscapeCharList().contains(charValue) == true) + { + builder.append("\\"); + } + builder.append(charValue); + } + + return builder.toString(); + } + + /** + * List of escape characters + */ + private static List ESCAPE_CHAR_LIST = null; + + /** + * Get the list of escape chars + * + * @return list of excape chars + */ + private List getEscapeCharList() + { + if (ESCAPE_CHAR_LIST == null) + { + //([{\^$|)?*+. + ESCAPE_CHAR_LIST = new ArrayList(4); + ESCAPE_CHAR_LIST.add('.'); + ESCAPE_CHAR_LIST.add('^'); + ESCAPE_CHAR_LIST.add('$'); + ESCAPE_CHAR_LIST.add('('); + ESCAPE_CHAR_LIST.add('['); + ESCAPE_CHAR_LIST.add('{'); + ESCAPE_CHAR_LIST.add('\\'); + ESCAPE_CHAR_LIST.add('|'); + ESCAPE_CHAR_LIST.add(')'); + ESCAPE_CHAR_LIST.add('?'); + ESCAPE_CHAR_LIST.add('+'); + } + return ESCAPE_CHAR_LIST; + } + + /** + * @see org.alfresco.repo.action.evaluator.compare.PropertyValueComparator#registerComparator(org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator) + */ + public void registerComparator(ComparePropertyValueEvaluator evaluator) + { + evaluator.registerComparator(DataTypeDefinition.TEXT, this); + } +} diff --git a/source/java/org/alfresco/repo/action/executer/ActionExecuter.java b/source/java/org/alfresco/repo/action/executer/ActionExecuter.java new file mode 100644 index 0000000000..c5c9cf8bb0 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ActionExecuter.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public interface ActionExecuter +{ + /** + * Get the action definition for the action + * + * @return the action definition + */ + public ActionDefinition getActionDefinition(); + + /** + * Execute the action executer + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + */ + public void execute( + Action action, + NodeRef actionedUponNodeRef); +} diff --git a/source/java/org/alfresco/repo/action/executer/ActionExecuterAbstractBase.java b/source/java/org/alfresco/repo/action/executer/ActionExecuterAbstractBase.java new file mode 100644 index 0000000000..72f70ba498 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ActionExecuterAbstractBase.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import org.alfresco.repo.action.ActionDefinitionImpl; +import org.alfresco.repo.action.ParameterizedItemAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Rule action executor abstract base. + * + * @author Roy Wetherall + */ +public abstract class ActionExecuterAbstractBase extends ParameterizedItemAbstractBase implements ActionExecuter +{ + /** + * Action definition + */ + protected ActionDefinition actionDefinition; + + /** + * Indicated whether the action is public or internal + */ + protected boolean publicAction = true; + + /** + * Init method + */ + public void init() + { + if (this.publicAction == true) + { + this.runtimeActionService.registerActionExecuter(this); + } + } + + /** + * Set whether the action is public or not. + * + * @param publicAction true if the action is public, false otherwise + */ + public void setPublicAction(boolean publicAction) + { + this.publicAction = publicAction; + } + + /** + * Get rule action definition + * + * @return the action definition object + */ + public ActionDefinition getActionDefinition() + { + if (this.actionDefinition == null) + { + this.actionDefinition = new ActionDefinitionImpl(this.name); + ((ActionDefinitionImpl)this.actionDefinition).setTitleKey(getTitleKey()); + ((ActionDefinitionImpl)this.actionDefinition).setDescriptionKey(getDescriptionKey()); + ((ActionDefinitionImpl)this.actionDefinition).setAdhocPropertiesAllowed(getAdhocPropertiesAllowed()); + ((ActionDefinitionImpl)this.actionDefinition).setRuleActionExecutor(this.name); + ((ActionDefinitionImpl)this.actionDefinition).setParameterDefinitions(getParameterDefintions()); + } + return this.actionDefinition; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + public void execute(Action action, NodeRef actionedUponNodeRef) + { + // Check the mandatory properties + checkMandatoryProperties(action, getActionDefinition()); + + // Execute the implementation + executeImpl(action, actionedUponNodeRef); + } + + /** + * Execute the action implementation + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node + */ + protected abstract void executeImpl(Action action, NodeRef actionedUponNodeRef); + + +} diff --git a/source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuter.java b/source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuter.java new file mode 100644 index 0000000000..557be6e6f1 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuter.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Add features action executor implementation. + * + * @author Roy Wetherall + */ +public class AddFeaturesActionExecuter extends ActionExecuterAbstractBase +{ + /** + * Action constants + */ + public static final String NAME = "add-features"; + public static final String PARAM_ASPECT_NAME = "aspect-name"; + public static final String PARAM_ASPECT_PROPERTIES = "aspect_properties"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Adhoc properties are allowed for this executor + */ + @Override + protected boolean getAdhocPropertiesAllowed() + { + return true; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + Map properties = new HashMap(); + QName aspectQName = null; + + Map paramValues = ruleAction.getParameterValues(); + for (Map.Entry entry : paramValues.entrySet()) + { + if (entry.getKey().equals(PARAM_ASPECT_NAME) == true) + { + aspectQName = (QName)entry.getValue(); + } + else + { + // Must be an adhoc property + QName propertyQName = QName.createQName(entry.getKey()); + Serializable propertyValue = entry.getValue(); + properties.put(propertyQName, propertyValue); + } + } + + // Add the aspect + this.nodeService.addAspect(actionedUponNodeRef, aspectQName, properties); + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_ASPECT_NAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASPECT_NAME))); + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuterTest.java b/source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuterTest.java new file mode 100644 index 0000000000..5c2b01ef59 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/AddFeaturesActionExecuterTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Add features action execution test + * + * @author Roy Wetherall + */ +public class AddFeaturesActionExecuterTest extends BaseSpringTest +{ + /** + * The node service + */ + private NodeService nodeService; + + /** + * The store reference + */ + private StoreRef testStoreRef; + + /** + * The root node reference + */ + private NodeRef rootNodeRef; + + /** + * The test node reference + */ + private NodeRef nodeRef; + + /** + * The add features action executer + */ + private AddFeaturesActionExecuter executer; + + /** + * Id used to identify the test action created + */ + private final static String ID = GUID.generate(); + + /** + * Called at the begining of all tests + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Get the executer instance + this.executer = (AddFeaturesActionExecuter)this.applicationContext.getBean(AddFeaturesActionExecuter.NAME); + } + + /** + * Test execution + */ + public void testExecution() + { + // Check that the node does not have the classifiable aspect + assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_CLASSIFIABLE)); + + // Execute the action + ActionImpl action = new ActionImpl(ID, AddFeaturesActionExecuter.NAME, null); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_CLASSIFIABLE); + this.executer.execute(action, this.nodeRef); + + // Check that the node now has the classifiable aspect applied + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_CLASSIFIABLE)); + } +} diff --git a/source/java/org/alfresco/repo/action/executer/CheckInActionExecuter.java b/source/java/org/alfresco/repo/action/executer/CheckInActionExecuter.java new file mode 100644 index 0000000000..366c97408c --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/CheckInActionExecuter.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionType; + +/** + * Check in action executor + * + * @author Roy Wetherall + */ +public class CheckInActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "check-in"; + public static final String PARAM_DESCRIPTION = "description"; + public static final String PARAM_MINOR_CHANGE = "minorChange"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The coci service + */ + private CheckOutCheckInService cociService; + + /** + * Set node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the checkIn checkOut service + * + * @param cociService the checkIn checkOut Service + */ + public void setCociService(CheckOutCheckInService cociService) + { + this.cociService = cociService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.ref.NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + // First ensure that the actionedUponNodeRef is a workingCopy + if (this.nodeService.exists(actionedUponNodeRef) == true && + this.nodeService.hasAspect(actionedUponNodeRef, ContentModel.ASPECT_WORKING_COPY) == true) + { + // Get the version description + String description = (String)ruleAction.getParameterValue(PARAM_DESCRIPTION); + Map versionProperties = new HashMap(1); + versionProperties.put(Version.PROP_DESCRIPTION, description); + + // determine whether the change is minor or major + Boolean minorChange = (Boolean)ruleAction.getParameterValue(PARAM_MINOR_CHANGE); + if (minorChange != null && minorChange.booleanValue() == false) + { + versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR); + } + else + { + versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MINOR); + } + + // TODO determine whether the document should be kept checked out + + // Check the node in + this.cociService.checkin(actionedUponNodeRef, versionProperties); + } + } + + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_DESCRIPTION, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_DESCRIPTION))); + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/CheckOutActionExecuter.java b/source/java/org/alfresco/repo/action/executer/CheckOutActionExecuter.java new file mode 100644 index 0000000000..16e5a5c45a --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/CheckOutActionExecuter.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Check out action executor + * + * @author Roy Wetherall + */ +public class CheckOutActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "check-out"; + public static final String PARAM_DESTINATION_FOLDER = "destination-folder"; + public static final String PARAM_ASSOC_TYPE_QNAME = "assoc-type"; + public static final String PARAM_ASSOC_QNAME = "assoc-name"; + + /** + * The version operations service + */ + private CheckOutCheckInService cociService; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the coci service + * + * @param cociService the coci service + */ + public void setCociService(CheckOutCheckInService cociService) + { + this.cociService = cociService; + } + + /** + * Add the parameter defintions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_DESTINATION_FOLDER, DataTypeDefinition.NODE_REF, false, getParamDisplayLabel(PARAM_DESTINATION_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_TYPE_QNAME, DataTypeDefinition.QNAME, false, getParamDisplayLabel(PARAM_ASSOC_TYPE_QNAME))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_QNAME, DataTypeDefinition.QNAME, false, getParamDisplayLabel(PARAM_ASSOC_QNAME))); + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.ref.NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true && + this.nodeService.hasAspect(actionedUponNodeRef, ContentModel.ASPECT_WORKING_COPY) == false) + { + // Get the destination details + NodeRef destinationParent = (NodeRef)ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER); + QName destinationAssocTypeQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_TYPE_QNAME); + QName destinationAssocQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_QNAME); + + if (destinationParent == null || destinationAssocTypeQName == null || destinationAssocQName == null) + { + // Check the node out to the current location + this.cociService.checkout(actionedUponNodeRef); + } + else + { + // Check the node out to the specified location + this.cociService.checkout( + actionedUponNodeRef, + destinationParent, + destinationAssocTypeQName, + destinationAssocQName); + } + } + } +} diff --git a/source/java/org/alfresco/repo/action/executer/CompositeActionExecuter.java b/source/java/org/alfresco/repo/action/executer/CompositeActionExecuter.java new file mode 100644 index 0000000000..cf27a674fa --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/CompositeActionExecuter.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Add features action executor implementation. + * + * @author Roy Wetherall + */ +public class CompositeActionExecuter extends ActionExecuterAbstractBase +{ + /** + * Action constants + */ + public static final String NAME = "composite-action"; + + /** + * The action service + */ + private RuntimeActionService actionService; + + /** + * Set the action service + * + * @param actionService the action service + */ + public void setActionService(RuntimeActionService actionService) + { + this.actionService = actionService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, NodeRef) + */ + public void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (action instanceof CompositeAction) + { + for (Action subAction : ((CompositeAction)action).getActions()) + { + // We don't check the conditions of sub-actions and they don't have an execution history + this.actionService.directActionExecution(subAction, actionedUponNodeRef); + } + } + } + + /** + * Add parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + // No parameters + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java new file mode 100644 index 0000000000..cbcd6c3ecb --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.metadata.MetadataExtracter; +import org.alfresco.repo.content.metadata.MetadataExtracterRegistry; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Extract metadata from any added content. + *

    + * The metadata is extracted from the content and compared to the current + * property values. Missing or zero-length properties are replaced, + * otherwise they are left as is.
    + * This may change if the action gets parameterized in future. + * + * @author Jesper Steen Møller + */ +public class ContentMetadataExtracter extends ActionExecuterAbstractBase +{ + /** + * Action constants + */ + public static final String NAME = "extract-metadata"; + + /* + * TODO: Action parameters. + * + * Currently none exist, but it may be nice to add a 'policy' parameter for + * overwriting the extracted properties, with the following possible values: + * 1) Never: Never overwrite node properties that + * exist (i.e. preserve values, nulls, and blanks) + * 2) Pragmatic: Write + * extracted properties if they didn't exist before, are null, or evaluate + * to an empty string. + * 3) Always: Always store the extracted properes. + * + * Policies 1 and 2 will preserve previously set properties in case nodes + * are moved/copied, making this action run on the same content several + * times. However, if a property is deliberately cleared (e.g. by putting + * the empty string into the "decription" field), the pragmatic policy would + * indeed overwrite it. The current implementation matches the 'pragmatic' + * policy. + */ + + /** + * The node service + */ + private NodeService nodeService; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Our content service + */ + private ContentService contentService; + + /** + * @param contentService The contentService to set. + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Our Extracter + */ + private MetadataExtracterRegistry metadataExtracterRegistry; + + /** + * @param metadataExtracterRegistry The metadataExtracterRegistry to set. + */ + public void setMetadataExtracterRegistry(MetadataExtracterRegistry metadataExtracterRegistry) + { + this.metadataExtracterRegistry = metadataExtracterRegistry; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, + * NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + ContentReader cr = contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT); + + // 'cr' may be null, e.g. for folders and the like + if (cr != null && cr.getMimetype() != null) + { + MetadataExtracter me = metadataExtracterRegistry.getExtracter(cr.getMimetype()); + if (me != null) + { + Map newProps = new HashMap(7, 0.5f); + me.extract(cr, newProps); + + Map allProps = nodeService.getProperties(actionedUponNodeRef); + + /* + * The code below implements a modestly conservative + * 'preserve' policy which shouldn't override values + * accidentally. + */ + + boolean changed = false; + for (QName key : newProps.keySet()) + { + Serializable value = newProps.get(key); + if (value == null) + continue; // Content extracters shouldn't do this + + // Look up the old value, and check for nulls + Serializable oldValue = allProps.get(key); + if (oldValue == null || oldValue.toString().length() == 0) + { + allProps.put(key, value); + changed = true; + } + } + // TODO: Should we be adding the associated aspects or is + // that done by the type system + // (or are ad-hoc properties allowed?) + if (changed) + nodeService.setProperties(actionedUponNodeRef, allProps); + } + } + } + } + + @Override + protected void addParameterDefintions(List arg0) + { + // None! + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTest.java b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTest.java new file mode 100644 index 0000000000..8e9cde27cc --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.metadata.MetadataExtracterRegistry; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Test of the ActionExecuter for extracting metadata. Note: This test makes + * assumptions about the PDF test data for PdfBoxExtracter. + * + * @author Jesper Steen Møller + */ +public class ContentMetadataExtracterTest extends BaseSpringTest +{ + protected static final String QUICK_TITLE = "The quick brown fox jumps over the lazy dog"; + protected static final String QUICK_DESCRIPTION = "Gym class featuring a brown fox and lazy dog"; + protected static final String QUICK_CREATOR = "Nevin Nollop"; + + private NodeService nodeService; + private ContentService contentService; + private MetadataExtracterRegistry metadataExtracterRegistry; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + + private ContentMetadataExtracter executer; + + private final static String ID = GUID.generate(); + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService) this.applicationContext.getBean("nodeService"); + this.contentService = (ContentService) this.applicationContext.getBean("contentService"); + this.metadataExtracterRegistry = (MetadataExtracterRegistry) this.applicationContext + .getBean("metadataExtracterRegistry"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Setup the content from the PDF test data + ContentWriter cw = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + cw.setMimetype(MimetypeMap.MIMETYPE_PDF); + cw.putContent(AbstractContentTransformerTest.loadQuickTestFile("pdf")); + + // Get the executer instance + this.executer = (ContentMetadataExtracter) this.applicationContext.getBean(ContentMetadataExtracter.NAME); + } + + /** + * Test execution of the extraction itself + */ + public void testFromBlanks() + { + // Test that the action writes properties when they don't exist or are + // unset + + // Get the old props + Map props = this.nodeService.getProperties(this.nodeRef); + props.remove(ContentModel.PROP_CREATOR); + props.put(ContentModel.PROP_TITLE, ""); + props.put(ContentModel.PROP_DESCRIPTION, null); // Wonder how this will + // be handled + this.nodeService.setProperties(this.nodeRef, props); + + // Execute the action + ActionImpl action = new ActionImpl(ID, SetPropertyValueActionExecuter.NAME, null); + + this.executer.execute(action, this.nodeRef); + + // Check that the properties have been set + assertEquals(QUICK_TITLE, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_TITLE)); + assertEquals(QUICK_DESCRIPTION, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_DESCRIPTION)); + assertEquals(QUICK_CREATOR, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_CREATOR)); + } + + /** + * Test execution of the pragmatic approach + */ + public void testFromPartial() + { + // Test that the action does not overwrite properties that are already + // set + String myCreator = "Null-op"; + String myTitle = "The hot dog is eaten by the city fox"; + + // Get the old props + Map props = this.nodeService.getProperties(this.nodeRef); + props.put(ContentModel.PROP_CREATOR, myCreator); + props.put(ContentModel.PROP_TITLE, myTitle); + props.remove(ContentModel.PROP_DESCRIPTION); // Allow this baby + this.nodeService.setProperties(this.nodeRef, props); + + // Execute the action + ActionImpl action = new ActionImpl(ID, SetPropertyValueActionExecuter.NAME, null); + + this.executer.execute(action, this.nodeRef); + + // Check that the properties have been preserved + assertEquals(myTitle, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_TITLE)); + assertEquals(myCreator, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_CREATOR)); + + // But this one should have been set + assertEquals(QUICK_DESCRIPTION, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_DESCRIPTION)); + + } + + // If we implement other policies than "pragmatic", they should be tested as + // well... +} diff --git a/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java b/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java new file mode 100644 index 0000000000..f28530c976 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +/** + * + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Copy action executor. + *

    + * Copies the actioned upon node to a specified location. + * + * @author Roy Wetherall + */ +public class CopyActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "copy"; + public static final String PARAM_DESTINATION_FOLDER = "destination-folder"; + public static final String PARAM_ASSOC_TYPE_QNAME = "assoc-type"; + public static final String PARAM_ASSOC_QNAME = "assoc-name"; + public static final String PARAM_DEEP_COPY = "deep-copy"; + + /** + * Node operations service + */ + private CopyService copyService; + + /** + * The node service + */ + private NodeService nodeService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setCopyService(CopyService copyService) + { + this.copyService = copyService; + } + + + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_DESTINATION_FOLDER, DataTypeDefinition.NODE_REF, true, getParamDisplayLabel(PARAM_DESTINATION_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_TYPE_QNAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASSOC_TYPE_QNAME))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_QNAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASSOC_QNAME))); + paramList.add(new ParameterDefinitionImpl(PARAM_DEEP_COPY, DataTypeDefinition.BOOLEAN, false, getParamDisplayLabel(PARAM_DEEP_COPY))); + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.ref.NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + NodeRef destinationParent = (NodeRef)ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER); + QName destinationAssocTypeQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_TYPE_QNAME); + QName destinationAssocQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_QNAME); + + // TODO get this from a parameter value + boolean deepCopy = false; + + this.copyService.copy( + actionedUponNodeRef, + destinationParent, + destinationAssocTypeQName, + destinationAssocQName, + deepCopy); + } + } +} diff --git a/source/java/org/alfresco/repo/action/executer/CreateVersionActionExecuter.java b/source/java/org/alfresco/repo/action/executer/CreateVersionActionExecuter.java new file mode 100644 index 0000000000..78d367a6e1 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/CreateVersionActionExecuter.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.version.VersionService; + +/** + * Add features action executor implementation. + * + * @author Roy Wetherall + */ +public class CreateVersionActionExecuter extends ActionExecuterAbstractBase +{ + /** + * Action constants + */ + public static final String NAME = "create-version"; + + public NodeService nodeService; + + public VersionService versionService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setVersionService(VersionService versionService) + { + this.versionService = versionService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true && + this.nodeService.hasAspect(actionedUponNodeRef, ContentModel.ASPECT_VERSIONABLE) == true) + { + // TODO would be nice to be able to set the version details + this.versionService.createVersion(actionedUponNodeRef, null); + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/ExporterActionExecuter.java b/source/java/org/alfresco/repo/action/executer/ExporterActionExecuter.java new file mode 100644 index 0000000000..c4f13c01fd --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ExporterActionExecuter.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.TempFileProvider; + +/** + * Exporter action executor + * + * @author gavinc + */ +public class ExporterActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "export"; + public static final String PARAM_STORE = "store"; + public static final String PARAM_PACKAGE_NAME = "package-name"; + public static final String PARAM_DESTINATION_FOLDER = "destination"; + public static final String PARAM_INCLUDE_CHILDREN = "include-children"; + public static final String PARAM_INCLUDE_SELF = "include-self"; + public static final String PARAM_ENCODING = "encoding"; + + private static final String TEMP_FILE_PREFIX = "alf"; + + /** + * The exporter service + */ + private ExporterService exporterService; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The content service + */ + private ContentService contentService; + + /** + * Sets the ExporterService to use + * + * @param exporterService The ExporterService + */ + public void setExporterService(ExporterService exporterService) + { + this.exporterService = exporterService; + } + + /** + * Sets the NodeService to use + * + * @param nodeService The NodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the ContentService to use + * + * @param contentService The ContentService + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.ref.NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + File zipFile = null; + try + { + String packageName = (String)ruleAction.getParameterValue(PARAM_PACKAGE_NAME); + File dataFile = new File(packageName); + File contentDir = new File(packageName); + + // create a temporary file to hold the zip + zipFile = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, ACPExportPackageHandler.ACP_EXTENSION); + ACPExportPackageHandler zipHandler = new ACPExportPackageHandler(new FileOutputStream(zipFile), + dataFile, contentDir); + + ExporterCrawlerParameters params = new ExporterCrawlerParameters(); + boolean includeChildren = true; + Boolean withKids = (Boolean)ruleAction.getParameterValue(PARAM_INCLUDE_CHILDREN); + if (withKids != null) + { + includeChildren = withKids.booleanValue(); + } + params.setCrawlChildNodes(includeChildren); + + boolean includeSelf = false; + Boolean andMe = (Boolean)ruleAction.getParameterValue(PARAM_INCLUDE_SELF); + if (andMe != null) + { + includeSelf = andMe.booleanValue(); + } + params.setCrawlSelf(includeSelf); + + params.setExportFrom(new Location(actionedUponNodeRef)); + + // perform the actual export + this.exporterService.exportView(zipHandler, params, null); + + // now the export is done we need to create a node in the repository + // to hold the exported package + NodeRef zip = createExportZip(ruleAction, actionedUponNodeRef); + ContentWriter writer = this.contentService.getWriter(zip, ContentModel.PROP_CONTENT, true); + writer.setEncoding((String)ruleAction.getParameterValue(PARAM_ENCODING)); + writer.setMimetype(MimetypeMap.MIMETYPE_ACP); + writer.putContent(zipFile); + } + catch (FileNotFoundException fnfe) + { + throw new ActionServiceException("export.package.error", fnfe); + } + finally + { + // try and delete the temporary file + if (zipFile != null) + { + zipFile.delete(); + } + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_PACKAGE_NAME, DataTypeDefinition.TEXT, true, + getParamDisplayLabel(PARAM_PACKAGE_NAME))); + paramList.add(new ParameterDefinitionImpl(PARAM_ENCODING, DataTypeDefinition.TEXT, true, + getParamDisplayLabel(PARAM_ENCODING))); + paramList.add(new ParameterDefinitionImpl(PARAM_STORE, DataTypeDefinition.TEXT, true, + getParamDisplayLabel(PARAM_STORE))); + paramList.add(new ParameterDefinitionImpl(PARAM_DESTINATION_FOLDER, DataTypeDefinition.NODE_REF, true, + getParamDisplayLabel(PARAM_DESTINATION_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_INCLUDE_CHILDREN, DataTypeDefinition.BOOLEAN, false, + getParamDisplayLabel(PARAM_INCLUDE_CHILDREN))); + paramList.add(new ParameterDefinitionImpl(PARAM_INCLUDE_SELF, DataTypeDefinition.BOOLEAN, false, + getParamDisplayLabel(PARAM_INCLUDE_SELF))); + } + + /** + * Creates the ZIP file node in the repository for the export + * + * @param ruleAction The rule being executed + * @return The NodeRef of the newly created ZIP file + */ + private NodeRef createExportZip(Action ruleAction, NodeRef actionedUponNodeRef) + { + // create a node in the repository to represent the export package + NodeRef exportDest = (NodeRef)ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER); + String packageName = (String)ruleAction.getParameterValue(PARAM_PACKAGE_NAME); + + // add the default Alfresco content package extension if an extension hasn't been given + if (packageName.indexOf(".") == -1) + { + packageName = packageName + "." + ACPExportPackageHandler.ACP_EXTENSION; + } + + // set the name for the new node + Map contentProps = new HashMap(1); + contentProps.put(ContentModel.PROP_NAME, packageName); + + // create the node to represent the zip file + String assocName = QName.createValidLocalName(packageName); + ChildAssociationRef assocRef = this.nodeService.createNode( + exportDest, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, assocName), + ContentModel.TYPE_CONTENT, contentProps); + + NodeRef zipNodeRef = assocRef.getChildRef(); + + // build a description string to be set on the node representing the content package + String desc = ""; + String storeRef = (String)ruleAction.getParameterValue(PARAM_STORE); + NodeRef rootNode = this.nodeService.getRootNode(new StoreRef(storeRef)); + if (rootNode.equals(actionedUponNodeRef)) + { + desc = I18NUtil.getMessage("export.root.package.description"); + } + else + { + String spaceName = (String)this.nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_NAME); + String pattern = I18NUtil.getMessage("export.package.description"); + if (pattern != null && spaceName != null) + { + desc = MessageFormat.format(pattern, spaceName); + } + } + + // apply the titled aspect to behave in the web client + Map titledProps = new HashMap(3, 1.0f); + titledProps.put(ContentModel.PROP_TITLE, packageName); + titledProps.put(ContentModel.PROP_DESCRIPTION, desc); + this.nodeService.addAspect(zipNodeRef, ContentModel.ASPECT_TITLED, titledProps); + + return zipNodeRef; + } +} diff --git a/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java b/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java new file mode 100644 index 0000000000..a17defdf76 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ImageTransformActionExecuter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.content.transform.magick.ImageMagickContentTransformer; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NoTransformerException; + +/** + * Transfor action executer + * + * @author Roy Wetherall + */ +public class ImageTransformActionExecuter extends TransformActionExecuter +{ + /** + * Action constants + */ + public static final String NAME = "transform-image"; + public static final String PARAM_CONVERT_COMMAND = "convert-command"; + + private ImageMagickContentTransformer imageMagickContentTransformer; + + /** + * Set the image magick content transformer + * + * @param imageMagickContentTransformer the conten transformer + */ + public void setImageMagickContentTransformer(ImageMagickContentTransformer imageMagickContentTransformer) + { + this.imageMagickContentTransformer = imageMagickContentTransformer; + } + + /** + * Add parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + super.addParameterDefintions(paramList); + paramList.add(new ParameterDefinitionImpl(PARAM_CONVERT_COMMAND, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_CONVERT_COMMAND))); + } + + /** + * @see org.alfresco.repo.action.executer.TransformActionExecuter#doTransform(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter) + */ + protected void doTransform(Action ruleAction, ContentReader contentReader, ContentWriter contentWriter) + { + // check if the transformer is going to work, i.e. is available + if (!this.imageMagickContentTransformer.isAvailable()) + { + throw new NoTransformerException(contentReader.getMimetype(), contentWriter.getMimetype()); + } + // Try and transform the content + String convertCommand = (String)ruleAction.getParameterValue(PARAM_CONVERT_COMMAND); + // create some options for the transform + Map options = new HashMap(5); + options.put(ImageMagickContentTransformer.KEY_OPTIONS, convertCommand); + + this.imageMagickContentTransformer.transform(contentReader, contentWriter, options); + } +} diff --git a/source/java/org/alfresco/repo/action/executer/ImporterActionExecuter.java b/source/java/org/alfresco/repo/action/executer/ImporterActionExecuter.java new file mode 100644 index 0000000000..172fde3f3f --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ImporterActionExecuter.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.File; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.importer.ACPImportPackageHandler; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.util.TempFileProvider; + +/** + * Importer action executor + * + * @author gavinc + */ +public class ImporterActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "import"; + public static final String PARAM_ENCODING = "encoding"; + public static final String PARAM_DESTINATION_FOLDER = "destination"; + + private static final String TEMP_FILE_PREFIX = "alf"; + private static final String TEMP_FILE_SUFFIX = ".acp"; + + /** + * The importer service + */ + private ImporterService importerService; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The content service + */ + private ContentService contentService; + + /** + * Sets the ImporterService to use + * + * @param importerService The ImporterService + */ + public void setImporterService(ImporterService importerService) + { + this.importerService = importerService; + } + + /** + * Sets the NodeService to use + * + * @param nodeService The NodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the ContentService to use + * + * @param contentService The ContentService + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.ref.NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // The node being passed in should be an Alfresco content package + ContentReader reader = this.contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT); + if (reader != null) + { + if (MimetypeMap.MIMETYPE_ACP.equals(reader.getMimetype())) + { + File zipFile = null; + try + { + // unfortunately a ZIP file can not be read directly from an input stream so we have to create + // a temporary file first + zipFile = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX); + reader.getContent(zipFile); + + ACPImportPackageHandler importHandler = new ACPImportPackageHandler(zipFile, + (String)ruleAction.getParameterValue(PARAM_ENCODING)); + NodeRef importDest = (NodeRef)ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER); + + this.importerService.importView(importHandler, new Location(importDest), null, null); + } + finally + { + // now the import is done, delete the temporary file + if (zipFile != null) + { + zipFile.delete(); + } + } + } + } + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_DESTINATION_FOLDER, DataTypeDefinition.NODE_REF, + true, getParamDisplayLabel(PARAM_DESTINATION_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_ENCODING, DataTypeDefinition.TEXT, + true, getParamDisplayLabel(PARAM_ENCODING))); + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/LinkCategoryActionExecuter.java b/source/java/org/alfresco/repo/action/executer/LinkCategoryActionExecuter.java new file mode 100644 index 0000000000..dbee2dd995 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/LinkCategoryActionExecuter.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Link category action executor + * + * @author Roy Wetherall + */ +public class LinkCategoryActionExecuter extends ActionExecuterAbstractBase +{ + /** + * Rule constants + */ + public static final String NAME = "link-category"; + public static final String PARAM_CATEGORY_ASPECT = "category-aspect"; + public static final String PARAM_CATEGORY_VALUE = "category-value"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The dictionary service + */ + private DictionaryService dictionaryService; + + /** + * Sets the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Add the parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_CATEGORY_ASPECT, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_CATEGORY_ASPECT))); + paramList.add(new ParameterDefinitionImpl(PARAM_CATEGORY_VALUE, DataTypeDefinition.NODE_REF, true, getParamDisplayLabel(PARAM_CATEGORY_VALUE))); + } + + /** + * Execute action implementation + */ + @Override + protected void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + // Double check that the node still exists + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // Get the rule parameter values + QName categoryAspect = (QName)ruleAction.getParameterValue(PARAM_CATEGORY_ASPECT); + NodeRef categoryValue = (NodeRef)ruleAction.getParameterValue(PARAM_CATEGORY_VALUE); + + // Check that the apect is classifiable and is currently applied to the node + if (this.dictionaryService.isSubClass(categoryAspect, ContentModel.ASPECT_CLASSIFIABLE) == true) + { + // Get the category property qname + QName categoryProperty = null; + Map propertyDefs = this.dictionaryService.getAspect(categoryAspect).getProperties(); + for (Map.Entry entry : propertyDefs.entrySet()) + { + if (DataTypeDefinition.CATEGORY.equals(entry.getValue().getDataType().getName()) == true) + { + // Found the category property + categoryProperty = entry.getKey(); + break; + } + } + + if (categoryAspect != null) + { + // Add the aspect setting the category property to the approptiate values + Map properties = new HashMap(); + properties.put(categoryProperty, categoryValue); + this.nodeService.addAspect(actionedUponNodeRef, categoryAspect, properties); + } + } + } + } +} diff --git a/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java b/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java new file mode 100644 index 0000000000..46658ca356 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/MailActionExecuter.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; + +/** + * Mail action executor implementation. + * + * @author Roy Wetherall + */ +public class MailActionExecuter extends ActionExecuterAbstractBase +{ + private static Log logger = LogFactory.getLog(MailActionExecuter.class); + + /** + * Action executor constants + */ + public static final String NAME = "mail"; + public static final String PARAM_TO = "to"; + public static final String PARAM_SUBJECT = "subject"; + public static final String PARAM_TEXT = "text"; + + /** + * From address + */ + public static final String FROM_ADDRESS = "alfresco_repository@alfresco.org"; + + /** + * The java mail sender + */ + private JavaMailSender javaMailSender; + + /** + * Set the java mail sender + * + * @param javaMailSender the java mail sender + */ + public void setMailService(JavaMailSender javaMailSender) + { + this.javaMailSender = javaMailSender; + } + + /** + * Execute the rule action + */ + @Override + protected void executeImpl( + Action ruleAction, + NodeRef actionedUponNodeRef) + { + // Create the simple mail message + SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); + simpleMailMessage.setTo((String)ruleAction.getParameterValue(PARAM_TO)); + simpleMailMessage.setSubject((String)ruleAction.getParameterValue(PARAM_SUBJECT)); + simpleMailMessage.setText((String)ruleAction.getParameterValue(PARAM_TEXT)); + simpleMailMessage.setFrom(FROM_ADDRESS); + + try + { + // Send the message + javaMailSender.send(simpleMailMessage); + } + catch (Throwable e) + { + // don't stop the action but let admins know email is not getting sent + logger.error("Failed to send email to " + (String)ruleAction.getParameterValue(PARAM_TO), e); + } + } + + /** + * Add the parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_TO, DataTypeDefinition.TEXT, true, getParamDisplayLabel(PARAM_TO))); + paramList.add(new ParameterDefinitionImpl(PARAM_SUBJECT, DataTypeDefinition.TEXT, true, getParamDisplayLabel(PARAM_SUBJECT))); + paramList.add(new ParameterDefinitionImpl(PARAM_TEXT, DataTypeDefinition.TEXT, true, getParamDisplayLabel(PARAM_TEXT))); + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/MoveActionExecuter.java b/source/java/org/alfresco/repo/action/executer/MoveActionExecuter.java new file mode 100644 index 0000000000..0a339ad64d --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/MoveActionExecuter.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Copy action executor. + *

    + * Copies the actioned upon node to a specified location. + * + * @author Roy Wetherall + */ +public class MoveActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "move"; + public static final String PARAM_DESTINATION_FOLDER = "destination-folder"; + public static final String PARAM_ASSOC_TYPE_QNAME = "assoc-type"; + public static final String PARAM_ASSOC_QNAME = "assoc-name"; + + /** + * Node service + */ + private NodeService nodeService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_DESTINATION_FOLDER, DataTypeDefinition.NODE_REF, true, getParamDisplayLabel(PARAM_DESTINATION_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_TYPE_QNAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASSOC_TYPE_QNAME))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_QNAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASSOC_QNAME))); + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.ref.NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + NodeRef destinationParent = (NodeRef)ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER); + QName destinationAssocTypeQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_TYPE_QNAME); + QName destinationAssocQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_QNAME); + + this.nodeService.moveNode( + actionedUponNodeRef, + destinationParent, + destinationAssocTypeQName, + destinationAssocQName); + } + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuter.java b/source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuter.java new file mode 100644 index 0000000000..0ce847616b --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuter.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Add features action executor implementation. + * + * @author Roy Wetherall + */ +public class SetPropertyValueActionExecuter extends ActionExecuterAbstractBase +{ + /** + * Action constants + */ + public static final String NAME = "set-property-value"; + public static final String PARAM_PROPERTY = "property"; + public static final String PARAM_VALUE = "value"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // Set the value of the property + this.nodeService.setProperty( + actionedUponNodeRef, + (QName)ruleAction.getParameterValue(PARAM_PROPERTY), + ruleAction.getParameterValue(PARAM_VALUE)); + } + } + + /** + * Add parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_PROPERTY, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_PROPERTY))); + paramList.add(new ParameterDefinitionImpl(PARAM_VALUE, DataTypeDefinition.ANY, true, getParamDisplayLabel(PARAM_VALUE))); + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuterTest.java b/source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuterTest.java new file mode 100644 index 0000000000..4335790827 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/SetPropertyValueActionExecuterTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Is sub class evaluator test + * + * @author Roy Wetherall + */ +public class SetPropertyValueActionExecuterTest extends BaseSpringTest +{ + private NodeService nodeService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private SetPropertyValueActionExecuter executer; + + private final static String ID = GUID.generate(); + + private final static String TEST_VALUE = "TestValue"; + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Get the executer instance + this.executer = (SetPropertyValueActionExecuter)this.applicationContext.getBean(SetPropertyValueActionExecuter.NAME); + } + + /** + * Test execution + */ + public void testExecution() + { + // Check that the property is empty + assertNull(this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_NAME)); + + // Execute the action + ActionImpl action = new ActionImpl(ID, SetPropertyValueActionExecuter.NAME, null); + action.setParameterValue(SetPropertyValueActionExecuter.PARAM_PROPERTY, ContentModel.PROP_NAME); + action.setParameterValue(SetPropertyValueActionExecuter.PARAM_VALUE, TEST_VALUE); + this.executer.execute(action, this.nodeRef); + + // Check that the property value has been set + assertEquals(TEST_VALUE, this.nodeService.getProperty(this.nodeRef, ContentModel.PROP_NAME)); + + // Check what happens when a bad property name is set + action.setParameterValue(SetPropertyValueActionExecuter.PARAM_PROPERTY, QName.createQName("{test}badProperty")); + + try + { + this.executer.execute(action, this.nodeRef); + fail("We would expect and exception to be thrown since the property name is invalid."); + } + catch (Throwable exception) + { + // Good .. we where expecting this + } + } +} diff --git a/source/java/org/alfresco/repo/action/executer/SimpleWorkflowActionExecuter.java b/source/java/org/alfresco/repo/action/executer/SimpleWorkflowActionExecuter.java new file mode 100644 index 0000000000..b317490b25 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/SimpleWorkflowActionExecuter.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Simple workflow action executor + * + * @author Roy Wetherall + */ +public class SimpleWorkflowActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "simple-workflow"; + public static final String PARAM_APPROVE_STEP = "approve-step"; + public static final String PARAM_APPROVE_FOLDER = "approve-folder"; + public static final String PARAM_APPROVE_MOVE = "approve-move"; + public static final String PARAM_REJECT_STEP = "reject-step"; + public static final String PARAM_REJECT_FOLDER = "reject-folder"; + public static final String PARAM_REJECT_MOVE = "reject-move"; + + private NodeService nodeService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_APPROVE_STEP, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_APPROVE_STEP))); + paramList.add(new ParameterDefinitionImpl(PARAM_APPROVE_FOLDER, DataTypeDefinition.NODE_REF, false, getParamDisplayLabel(PARAM_APPROVE_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_APPROVE_MOVE, DataTypeDefinition.BOOLEAN, false, getParamDisplayLabel(PARAM_APPROVE_MOVE))); + paramList.add(new ParameterDefinitionImpl(PARAM_REJECT_STEP, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_REJECT_STEP))); + paramList.add(new ParameterDefinitionImpl(PARAM_REJECT_FOLDER, DataTypeDefinition.NODE_REF, false, getParamDisplayLabel(PARAM_REJECT_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_REJECT_MOVE, DataTypeDefinition.BOOLEAN, false, getParamDisplayLabel(PARAM_REJECT_MOVE))); + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl( + Action ruleAction, + NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // Get the parameter values + String approveStep = (String)ruleAction.getParameterValue(PARAM_APPROVE_STEP); + NodeRef approveFolder = (NodeRef)ruleAction.getParameterValue(PARAM_APPROVE_FOLDER); + Boolean approveMove = (Boolean)ruleAction.getParameterValue(PARAM_APPROVE_MOVE); + String rejectStep = (String)ruleAction.getParameterValue(PARAM_REJECT_STEP); + NodeRef rejectFolder = (NodeRef)ruleAction.getParameterValue(PARAM_REJECT_FOLDER); + Boolean rejectMove = (Boolean)ruleAction.getParameterValue(PARAM_REJECT_MOVE); + + // Set the property values + Map propertyValues = new HashMap(); + propertyValues.put(ContentModel.PROP_APPROVE_STEP, approveStep); + propertyValues.put(ContentModel.PROP_APPROVE_FOLDER, approveFolder); + if (approveMove != null) + { + propertyValues.put(ContentModel.PROP_APPROVE_MOVE, approveMove.booleanValue()); + } + propertyValues.put(ContentModel.PROP_REJECT_STEP, rejectStep); + propertyValues.put(ContentModel.PROP_REJECT_FOLDER, rejectFolder); + if (rejectMove != null) + { + propertyValues.put(ContentModel.PROP_REJECT_MOVE, rejectMove.booleanValue()); + } + + // Apply the simple workflow aspect to the node + this.nodeService.addAspect(actionedUponNodeRef, ContentModel.ASPECT_SIMPLE_WORKFLOW, propertyValues); + } + } +} diff --git a/source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuter.java b/source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuter.java new file mode 100644 index 0000000000..54912e4a06 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuter.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Add features action executor implementation. + * + * @author Roy Wetherall + */ +public class SpecialiseTypeActionExecuter extends ActionExecuterAbstractBase +{ + /** + * Action constants + */ + public static final String NAME = "specialise-type"; + public static final String PARAM_TYPE_NAME = "type-name"; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The dictionary service + */ + private DictionaryService dictionaryService; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef, NodeRef) + */ + public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == true) + { + // Get the type of the node + QName currentType = this.nodeService.getType(actionedUponNodeRef); + QName destinationType = (QName)ruleAction.getParameterValue(PARAM_TYPE_NAME); + + // Ensure that we are performing a specialise + if (currentType.equals(destinationType) == false && + this.dictionaryService.isSubClass(destinationType, currentType) == true) + { + // Specialise the type of the node + this.nodeService.setType(actionedUponNodeRef, destinationType); + } + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefintions(java.util.List) + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_TYPE_NAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_TYPE_NAME))); + } + +} diff --git a/source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuterTest.java b/source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuterTest.java new file mode 100644 index 0000000000..101ef05f20 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/SpecialiseTypeActionExecuterTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.GUID; + +/** + * Specialise type action execution test + * + * @author Roy Wetherall + */ +public class SpecialiseTypeActionExecuterTest extends BaseAlfrescoSpringTest +{ + /** + * The test node reference + */ + private NodeRef nodeRef; + + /** + * The specialise action executer + */ + private SpecialiseTypeActionExecuter executer; + + /** + * Id used to identify the test action created + */ + private final static String ID = GUID.generate(); + + /** + * Called at the begining of all tests + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Get the executer instance + this.executer = (SpecialiseTypeActionExecuter)this.applicationContext.getBean(SpecialiseTypeActionExecuter.NAME); + } + + /** + * Test execution + */ + public void testExecution() + { + // Check the type of the node + assertEquals(ContentModel.TYPE_CONTENT, this.nodeService.getType(this.nodeRef)); + + // Execute the action + ActionImpl action = new ActionImpl(ID, SpecialiseTypeActionExecuter.NAME, null); + action.setParameterValue(SpecialiseTypeActionExecuter.PARAM_TYPE_NAME, ContentModel.TYPE_FOLDER); + this.executer.execute(action, this.nodeRef); + + // Check that the node's type has not been changed since it would not be a specialisation + assertEquals(ContentModel.TYPE_CONTENT, this.nodeService.getType(this.nodeRef)); + + // Execute the action agian + action.setParameterValue(SpecialiseTypeActionExecuter.PARAM_TYPE_NAME, ContentModel.TYPE_DICTIONARY_MODEL); + this.executer.execute(action, this.nodeRef); + + // Check that the node's type has now been changed + assertEquals(ContentModel.TYPE_DICTIONARY_MODEL, this.nodeService.getType(this.nodeRef)); + } +} diff --git a/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java b/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java new file mode 100644 index 0000000000..4d28fea482 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/TransformActionExecuter.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.action.executer; + +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NoTransformerException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Transfor action executer + * + * @author Roy Wetherall + */ +public class TransformActionExecuter extends ActionExecuterAbstractBase +{ + /** + * The logger + */ + private static Log logger = LogFactory.getLog(TransformActionExecuter.class); + + /** + * Action constants + */ + public static final String NAME = "transform"; + public static final String PARAM_MIME_TYPE = "mime-type"; + public static final String PARAM_DESTINATION_FOLDER = "destination-folder"; + public static final String PARAM_ASSOC_TYPE_QNAME = "assoc-type"; + public static final String PARAM_ASSOC_QNAME = "assoc-name"; + + private DictionaryService dictionaryService; + private NodeService nodeService; + private ContentService contentService; + private CopyService copyService; + private MimetypeService mimetypeService; + + /** + * Set the mime type service + * + * @param mimetypeService the mime type service + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + /** + * Set the node service + * + * @param nodeService set the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set the content service + * + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Set the copy service + * + * @param copyService the copy service + */ + public void setCopyService(CopyService copyService) + { + this.copyService = copyService; + } + + /** + * Add parameter definitions + */ + @Override + protected void addParameterDefintions(List paramList) + { + paramList.add(new ParameterDefinitionImpl(PARAM_MIME_TYPE, DataTypeDefinition.TEXT, true, getParamDisplayLabel(PARAM_MIME_TYPE))); + paramList.add(new ParameterDefinitionImpl(PARAM_DESTINATION_FOLDER, DataTypeDefinition.NODE_REF, true, getParamDisplayLabel(PARAM_DESTINATION_FOLDER))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_TYPE_QNAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASSOC_TYPE_QNAME))); + paramList.add(new ParameterDefinitionImpl(PARAM_ASSOC_QNAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASSOC_QNAME))); + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl( + Action ruleAction, + NodeRef actionedUponNodeRef) + { + if (this.nodeService.exists(actionedUponNodeRef) == false) + { + // node doesn't exist - can't do anything + return; + } + // First check that the node is a sub-type of content + QName typeQName = this.nodeService.getType(actionedUponNodeRef); + if (this.dictionaryService.isSubClass(typeQName, ContentModel.TYPE_CONTENT) == false) + { + // it is not content, so can't transform + return; + } + // Get the mime type + String mimeType = (String)ruleAction.getParameterValue(PARAM_MIME_TYPE); + + // Get the details of the copy destination + NodeRef destinationParent = (NodeRef)ruleAction.getParameterValue(PARAM_DESTINATION_FOLDER); + QName destinationAssocTypeQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_TYPE_QNAME); + QName destinationAssocQName = (QName)ruleAction.getParameterValue(PARAM_ASSOC_QNAME); + + // Copy the content node + NodeRef copyNodeRef = this.copyService.copy( + actionedUponNodeRef, + destinationParent, + destinationAssocTypeQName, + destinationAssocQName, + false); + + + // Get the content reader + ContentReader contentReader = this.contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT); + if (contentReader == null) + { + // for some reason, this action is premature + throw new AlfrescoRuntimeException( + "Attempting to execute content transformation rule " + + "but content has not finished writing, i.e. no URL is available."); + } + String originalMimetype = contentReader.getMimetype(); + + // get the writer and set it up + ContentWriter contentWriter = this.contentService.getWriter(copyNodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype(mimeType); // new mimetype + contentWriter.setEncoding(contentReader.getEncoding()); // original encoding + + // Adjust the name of the copy + String originalName = (String)nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_NAME); + String newName = transformName(originalName, originalMimetype, mimeType); + nodeService.setProperty(copyNodeRef, ContentModel.PROP_NAME, newName); + String originalTitle = (String)nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_TITLE); + if (originalTitle != null && originalTitle.length() > 0) + { + String newTitle = transformName(originalTitle, originalMimetype, mimeType); + nodeService.setProperty(copyNodeRef, ContentModel.PROP_TITLE, newTitle); + } + + // Try and transform the content + try + { + doTransform(ruleAction, contentReader, contentWriter); + } + catch(NoTransformerException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("No transformer found to execute rule: \n" + + " reader: " + contentReader + "\n" + + " writer: " + contentWriter + "\n" + + " action: " + this); + } + // TODO: Revisit this for alternative solutions + nodeService.deleteNode(copyNodeRef); + } + } + + protected void doTransform(Action ruleAction, ContentReader contentReader, ContentWriter contentWriter) + { + this.contentService.transform(contentReader, contentWriter); + } + + /** + * Transform name from original extension to new extension + * + * @param original + * @param originalMimetype + * @param newMimetype + * @return + */ + private String transformName(String original, String originalMimetype, String newMimetype) + { + // get the current extension + int dotIndex = original.lastIndexOf('.'); + StringBuilder sb = new StringBuilder(original.length()); + if (dotIndex > -1) + { + // we found it + sb.append(original.substring(0, dotIndex)); + } + else + { + // no extension + sb.append(original); + } + // add the new extension + String newExtension = mimetypeService.getExtension(newMimetype); + sb.append('.').append(newExtension); + // done + return sb.toString(); + } + +} diff --git a/source/java/org/alfresco/repo/audit/AuditableAspect.java b/source/java/org/alfresco/repo/audit/AuditableAspect.java new file mode 100644 index 0000000000..52590e3e6c --- /dev/null +++ b/source/java/org/alfresco/repo/audit/AuditableAspect.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.audit; + +import java.util.Date; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.Behaviour; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * This aspect maintains the audit properties of the Auditable aspect. + * + * @author David Caruana + */ +public class AuditableAspect +{ + // Logger + private static final Log logger = LogFactory.getLog(AuditableAspect.class); + + // Unknown user, for when authentication has not occured + private static final String USERNAME_UNKNOWN = "unknown"; + + // Dependencies + private NodeService nodeService; + private AuthenticationService authenticationService; + private PolicyComponent policyComponent; + + // Behaviours + private Behaviour onCreateAudit; + private Behaviour onAddAudit; + private Behaviour onUpdateAudit; + + + /** + * @param nodeService the node service to use for audit property maintenance + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param authenticationService the authentication service + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * Initialise the Auditable Aspect + */ + public void init() + { + // Create behaviours + onCreateAudit = new JavaBehaviour(this, "onCreateAudit"); + onAddAudit = new JavaBehaviour(this, "onAddAudit"); + onUpdateAudit = new JavaBehaviour(this, "onUpdateAudit"); + + // Bind behaviours to node policies + policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), ContentModel.ASPECT_AUDITABLE, onCreateAudit); + policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), ContentModel.ASPECT_AUDITABLE, onAddAudit); + policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateNode"), ContentModel.ASPECT_AUDITABLE, onUpdateAudit); + + // Register onCopy class behaviour + policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), ContentModel.ASPECT_AUDITABLE, new JavaBehaviour(this, "onCopy")); + } + + /** + * Maintain audit properties on creation of Node + * + * @param childAssocRef the association to the child created + */ + public void onCreateAudit(ChildAssociationRef childAssocRef) + { + NodeRef nodeRef = childAssocRef.getChildRef(); + onAddAudit(nodeRef, null); + } + + /** + * Maintain audit properties on addition of audit aspect to a node + * + * @param nodeRef the node to which auditing has been added + * @param aspect the aspect added + */ + public void onAddAudit(NodeRef nodeRef, QName aspect) + { + try + { + onUpdateAudit.disable(); + + // Set created / updated date + Date now = new Date(System.currentTimeMillis()); + nodeService.setProperty(nodeRef, ContentModel.PROP_CREATED, now); + nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIED, now); + + // Set creator (but do not override, if explicitly set) + String creator = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_CREATOR); + if (creator == null || creator.length() == 0) + { + creator = getUsername(); + nodeService.setProperty(nodeRef, ContentModel.PROP_CREATOR, creator); + } + nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIER, creator); + + if (logger.isDebugEnabled()) + logger.debug("Auditable node " + nodeRef + " created [created,modified=" + now + ";creator,modifier=" + creator + "]"); + } + finally + { + onUpdateAudit.enable(); + } + } + + /** + * Maintain audit properties on update of node + * + * @param nodeRef the updated node + */ + public void onUpdateAudit(NodeRef nodeRef) + { + // Set updated date + Date now = new Date(System.currentTimeMillis()); + nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIED, now); + + // Set modifier + String modifier = getUsername(); + nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIER, modifier); + + if (logger.isDebugEnabled()) + logger.debug("Auditable node " + nodeRef + " updated [modified=" + now + ";modifier=" + modifier + "]"); + } + + /** + * @return the current username (or unknown, if unknown) + */ + private String getUsername() + { + String currentUserName = authenticationService.getCurrentUserName(); + if (currentUserName != null) + { + return currentUserName; + } + return USERNAME_UNKNOWN; + } + + /** + * OnCopy behaviour implementation for the lock aspect. + *

    + * Ensures that the propety values of the lock aspect are not copied onto + * the destination node. + * + * @see org.alfresco.repo.copy.CopyServicePolicies.OnCopyNodePolicy#onCopyNode(QName, NodeRef, StoreRef, boolean, PolicyScope) + */ + public void onCopy( + QName sourceClassRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + // The auditable aspect should not be copied + } +} diff --git a/source/java/org/alfresco/repo/audit/AuditableAspectTest.java b/source/java/org/alfresco/repo/audit/AuditableAspectTest.java new file mode 100644 index 0000000000..83e84f55d4 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/AuditableAspectTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.audit; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.debug.NodeStoreInspector; + +/** + * Checks that the behaviour of the {@link org.alfresco.repo.audit.AuditableAspect auditable aspect} + * is correct. + * + * @author Roy Wetherall + */ +public class AuditableAspectTest extends BaseSpringTest +{ + /** + * Services used by the tests + */ + private NodeService nodeService; + + /** + * Data used by the tests + */ + private StoreRef storeRef; + private NodeRef rootNodeRef; + + /** + * On setup in transaction implementation + */ + @Override + protected void onSetUpInTransaction() + throws Exception + { + // Set the services + this.nodeService = (NodeService)this.applicationContext.getBean("dbNodeService"); + + // Create the store and get the root node reference + this.storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(storeRef); + } + + + public void testAudit() + { + // Create a folder + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testfolder"), + ContentModel.TYPE_FOLDER); + + // Assert auditable properties exist on folder + assertAuditableProperties(childAssocRef.getChildRef()); + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + } + + + public void testNoAudit() + { + // Create a person (which doesn't have auditable capability by default) + Map personProps = new HashMap(); + personProps.put(ContentModel.PROP_USERNAME, "test person"); + personProps.put(ContentModel.PROP_HOMEFOLDER, rootNodeRef); + personProps.put(ContentModel.PROP_FIRSTNAME, "test first name"); + personProps.put(ContentModel.PROP_LASTNAME, "test last name"); + + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testperson"), + ContentModel.TYPE_PERSON, + personProps); + + // Assert the person is not auditable + Set aspects = nodeService.getAspects(childAssocRef.getChildRef()); + assertFalse(aspects.contains(ContentModel.ASPECT_AUDITABLE)); + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + } + + + public void testAddAudit() + { + // Create a person + Map personProps = new HashMap(); + personProps.put(ContentModel.PROP_USERNAME, "test person"); + personProps.put(ContentModel.PROP_HOMEFOLDER, rootNodeRef); + personProps.put(ContentModel.PROP_FIRSTNAME, "test first name"); + personProps.put(ContentModel.PROP_LASTNAME, "test last name"); + + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testperson"), + ContentModel.TYPE_PERSON, + personProps); + + // Assert the person is not auditable + Set aspects = nodeService.getAspects(childAssocRef.getChildRef()); + assertFalse(aspects.contains(ContentModel.ASPECT_AUDITABLE)); + + // Add auditable capability + nodeService.addAspect(childAssocRef.getChildRef(), ContentModel.ASPECT_AUDITABLE, null); + + nodeService.addAspect(childAssocRef.getChildRef(), ContentModel.ASPECT_TITLED, null); + + // Assert the person is now audiable + aspects = nodeService.getAspects(childAssocRef.getChildRef()); + assertTrue(aspects.contains(ContentModel.ASPECT_AUDITABLE)); + + // Assert the person's auditable property + assertAuditableProperties(childAssocRef.getChildRef()); + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + } + + + public void testAddAspect() + { + // Create a person (which doesn't have auditable capability by default) + Map personProps = new HashMap(); + personProps.put(ContentModel.PROP_USERNAME, "test person"); + personProps.put(ContentModel.PROP_HOMEFOLDER, rootNodeRef); + personProps.put(ContentModel.PROP_FIRSTNAME, "test first name "); + personProps.put(ContentModel.PROP_LASTNAME, "test last name"); + + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testperson"), + ContentModel.TYPE_PERSON, + personProps); + + // Add auditable capability + nodeService.addAspect(childAssocRef.getChildRef(), ContentModel.ASPECT_TITLED, null); + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + } + + + private void assertAuditableProperties(NodeRef nodeRef) + { + Map props = nodeService.getProperties(nodeRef); + assertNotNull(props.get(ContentModel.PROP_CREATED)); + assertNotNull(props.get(ContentModel.PROP_MODIFIED)); + assertNotNull(props.get(ContentModel.PROP_CREATOR)); + assertNotNull(props.get(ContentModel.PROP_MODIFIER)); + } + +} diff --git a/source/java/org/alfresco/repo/cache/CacheTest.java b/source/java/org/alfresco/repo/cache/CacheTest.java new file mode 100644 index 0000000000..ac0ca40536 --- /dev/null +++ b/source/java/org/alfresco/repo/cache/CacheTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.cache; + +import java.io.Serializable; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import net.sf.ehcache.CacheManager; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.repo.cache.EhCacheAdapter + * + * @author Derek Hulley + */ +public class CacheTest extends TestCase +{ + private static ApplicationContext ctx =new ClassPathXmlApplicationContext( + new String[] {"classpath:cache-test-context.xml", ApplicationContextHelper.CONFIG_LOCATIONS[0]} + ); + + private ServiceRegistry serviceRegistry; + private SimpleCache standaloneCache; + private SimpleCache backingCache; + private SimpleCache transactionalCache; + + @SuppressWarnings("unchecked") + @Override + public void setUp() throws Exception + { + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + standaloneCache = (SimpleCache) ctx.getBean("ehCache1"); + backingCache = (SimpleCache) ctx.getBean("backingCache"); + transactionalCache = (SimpleCache) ctx.getBean("transactionalCache"); + } + + public void testSetUp() throws Exception + { + CacheManager cacheManager = (CacheManager) ctx.getBean("ehCacheManager"); + assertNotNull(cacheManager); + CacheManager cacheManagerCheck = (CacheManager) ctx.getBean("ehCacheManager"); + assertTrue(cacheManager == cacheManagerCheck); + + assertNotNull(serviceRegistry); + assertNotNull(backingCache); + assertNotNull(standaloneCache); + assertNotNull(transactionalCache); + } + + public void testEhcacheAdaptors() throws Exception + { + backingCache.put("A", "AAA"); + assertNull("Second cache should not have first's present", standaloneCache.get("A")); + + assertEquals("AAA", backingCache.get("A")); + + backingCache.remove("A"); + assertNull(backingCache.get("A")); + } + + public void testTransactionalCacheNoTxn() throws Exception + { + String key = "B"; + String value = "BBB"; + // no transaction - do a put + transactionalCache.put(key, value); + // check that the value appears in the backing cache, backingCache + assertEquals("Backing cache not used for put when no transaction present", value, backingCache.get(key)); + + // remove the value from the backing cache and check that it is removed from the transaction cache + backingCache.remove(key); + assertNull("Backing cache not used for removed when no transaction present", transactionalCache.get(key)); + + // add value into backing cache + backingCache.put(key, value); + // remove it from the transactional cache + transactionalCache.remove(key); + // check that it is gone from the backing cache + assertNull("Non-transactional remove didn't go to backing cache", backingCache.get(key)); + } + + public void testTransactionalCacheWithTxn() throws Throwable + { + String newGlobalOne = "new_global_one"; + String newGlobalTwo = "new_global_two"; + String newGlobalThree = "new_global_three"; + String updatedTxnThree = "updated_txn_three"; + + // add item to global cache + backingCache.put(newGlobalOne, newGlobalOne); + backingCache.put(newGlobalTwo, newGlobalTwo); + backingCache.put(newGlobalThree, newGlobalThree); + + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction txn = transactionService.getUserTransaction(); + // begin a transaction + txn.begin(); + + try + { + // remove 1 from the cache + transactionalCache.remove(newGlobalOne); + assertFalse("Item was not removed from txn cache", transactionalCache.contains(newGlobalOne)); + assertNull("Get didn't return null", transactionalCache.get(newGlobalOne)); + assertTrue("Item was removed from backing cache", backingCache.contains(newGlobalOne)); + + // update 3 in the cache + transactionalCache.put(updatedTxnThree, "XXX"); + assertEquals("Item not updated in txn cache", "XXX", transactionalCache.get(updatedTxnThree)); + assertFalse("Item was put into backing cache", backingCache.contains(updatedTxnThree)); + + // commit the transaction + txn.commit(); + + // check that backing cache was updated with the in-transaction changes + assertFalse("Item was not removed from backing cache", backingCache.contains(newGlobalOne)); + assertNull("Item could still be fetched from backing cache", backingCache.get(newGlobalOne)); + assertEquals("Item not updated in backing cache", "XXX", backingCache.get(updatedTxnThree)); + } + catch (Throwable e) + { + if (txn.getStatus() == Status.STATUS_ACTIVE) + { + txn.rollback(); + } + throw e; + } + } + + /** + * Preloads the cache, then performs a simultaneous addition of N new values and + * removal of the N preloaded values. + * + * @param cache + * @param objectCount + * @return Returns the time it took in nanoseconds. + */ + public long runPerformanceTestOnCache(SimpleCache cache, int objectCount) + { + // preload + for (int i = 0; i < objectCount; i++) + { + String key = Integer.toString(i); + Integer value = new Integer(i); + cache.put(key, value); + } + + // start timer + long start = System.nanoTime(); + for (int i = 0; i < objectCount; i++) + { + String key = Integer.toString(i); + cache.remove(key); + // add a new value + key = Integer.toString(i + objectCount); + Integer value = new Integer(i + objectCount); + cache.put(key, value); + } + // stop + long stop = System.nanoTime(); + + return (stop - start); + } + + /** + * Tests a straight Ehcache adapter against a transactional cache both in and out + * of a transaction. This is done repeatedly, pushing the count up. + */ + public void testPerformance() throws Exception + { + for (int i = 0; i < 5; i++) + { + int count = (int) Math.pow(10D, (double)i); + + // test standalone + long timePlain = runPerformanceTestOnCache(standaloneCache, count); + + // do transactional cache in a transaction + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction txn = transactionService.getUserTransaction(); + txn.begin(); + long timeTxn = runPerformanceTestOnCache(transactionalCache, count); + long commitStart = System.nanoTime(); + txn.commit(); + long commitEnd = System.nanoTime(); + long commitTime = (commitEnd - commitStart); + // add this to the cache's performance overhead + timeTxn += commitTime; + + // report + System.out.println("Cache performance test: \n" + + " count: " + count + "\n" + + " direct: " + timePlain/((long)count) + " ns\\count \n" + + " transaction: " + timeTxn/((long)count) + " ns\\count"); + } + } +} diff --git a/source/java/org/alfresco/repo/cache/EhCacheAdapter.java b/source/java/org/alfresco/repo/cache/EhCacheAdapter.java new file mode 100644 index 0000000000..95fdb60f6c --- /dev/null +++ b/source/java/org/alfresco/repo/cache/EhCacheAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.cache; + +import java.io.IOException; +import java.io.Serializable; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.Element; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * A thin adapter for Ehcache support. + *

    + * Thread-safety is taken care of by the underlying Ehcache + * instance. + * + * @see org.springframework.cache.ehcache.EhCacheFactoryBean + * @see org.springframework.cache.ehcache.EhCacheManagerFactoryBean + * + * @author Derek Hulley + */ +public class EhCacheAdapter + implements SimpleCache +{ + private net.sf.ehcache.Cache cache; + + public EhCacheAdapter() + { + } + + /** + * @param cache the backing Ehcache instance + */ + public void setCache(Cache cache) + { + this.cache = cache; + } + + public boolean contains(K key) + { + try + { + return (cache.get(key) != null); + } + catch (CacheException e) + { + throw new AlfrescoRuntimeException("contains failed", e); + } + } + + @SuppressWarnings("unchecked") + public V get(K key) + { + try + { + Element element = cache.get(key); + if (element != null) + { + return (V) element.getValue(); + } + else + { + return null; + } + } + catch (CacheException e) + { + throw new AlfrescoRuntimeException("Failed to get from EhCache: \n" + + " key: " + key); + } + } + + public void put(K key, V value) + { + Element element = new Element(key, value); + cache.put(element); + } + + public void remove(K key) + { + cache.remove(key); + } + + public void clear() + { + try + { + cache.removeAll(); + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to clear cache", e); + } + } +} diff --git a/source/java/org/alfresco/repo/cache/SimpleCache.java b/source/java/org/alfresco/repo/cache/SimpleCache.java new file mode 100644 index 0000000000..ef644120bd --- /dev/null +++ b/source/java/org/alfresco/repo/cache/SimpleCache.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.cache; + +import java.io.Serializable; + +/** + * Basic caching interface. + *

    + * All implementations must be thread-safe. Additionally, the use of the + * Serializable for both keys and values ensures that the underlying + * cache implementations can support both clustered caches as well as persistent + * caches. + * + * @author Derek Hulley + */ +public interface SimpleCache +{ + public boolean contains(K key); + + public V get(K key); + + public void put(K key, V value); + + public void remove(K key); + + public void clear(); +} diff --git a/source/java/org/alfresco/repo/cache/TransactionalCache.java b/source/java/org/alfresco/repo/cache/TransactionalCache.java new file mode 100644 index 0000000000..2c70163723 --- /dev/null +++ b/source/java/org/alfresco/repo/cache/TransactionalCache.java @@ -0,0 +1,570 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.cache; + +import java.io.IOException; +import java.io.Serializable; +import java.util.List; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.util.EqualsHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * A 2-level cache that mainains both a transaction-local cache and + * wraps a non-transactional (shared) cache. + *

    + * It uses the Ehcache Cache for it's per-transaction + * caches as these provide automatic size limitations, etc. + *

    + * Instances of this class do not require a transaction. They will work + * directly with the shared cache when no transaction is present. There is + * virtually no overhead when running out-of-transaction. + *

    + * 3 caches are maintained. + *

      + *
    • Shared backing cache that should only be accessed by instances of this class
    • + *
    • Lazily created cache of updates made during the transaction
    • + *
    • Lazily created cache of deletions made during the transaction
    • + *
    + *

    + * When the cache is {@link #clear() cleared}, a flag is set on the transaction. + * The shared cache, instead of being cleared itself, is just ignored for the remainder + * of the tranasaction. At the end of the transaction, if the flag is set, the + * shared transaction is cleared before updates are added back to it. + *

    + * Because there is a limited amount of space available to the in-transaction caches, + * when either of these becomes full, the cleared flag is set. This ensures that + * the shared cache will not have stale data in the event of the transaction-local + * caches dropping items. + * + * @author Derek Hulley + */ +public class TransactionalCache + implements SimpleCache, TransactionListener, InitializingBean +{ + private static final String RESOURCE_KEY_TXN_DATA = "TransactionalCache.TxnData"; + private static final String VALUE_DELETE = "TransactionalCache.DeleteMarker"; + + private static Log logger = LogFactory.getLog(TransactionalCache.class); + + /** a name used to uniquely identify the transactional caches */ + private String name; + + /** the shared cache that will get updated after commits */ + private SimpleCache sharedCache; + + /** the manager to control Ehcache caches */ + private CacheManager cacheManager; + + /** the maximum number of elements to be contained in the cache */ + private int maxCacheSize = 500; + + /** a unique string identifying this instance when binding resources */ + private String resourceKeyTxnData; + + /** + * @see #setName(String) + */ + public String toString() + { + return name; + } + + public boolean equals(Object obj) + { + if (obj == this) + { + return true; + } + if (obj == null) + { + return false; + } + if (!(obj instanceof TransactionalCache)) + { + return false; + } + TransactionalCache that = (TransactionalCache) obj; + return EqualsHelper.nullSafeEquals(this.name, that.name); + } + + public int hashCode() + { + return name.hashCode(); + } + + /** + * Set the shared cache to use during transaction synchronization or when no transaction + * is present. + * + * @param sharedCache + */ + public void setSharedCache(SimpleCache sharedCache) + { + this.sharedCache = sharedCache; + } + + /** + * Set the manager to activate and control the cache instances + * + * @param cacheManager + */ + public void setCacheManager(CacheManager cacheManager) + { + this.cacheManager = cacheManager; + } + + /** + * Set the maximum number of elements to store in the update and remove caches. + * The maximum number of elements stored in the transaction will be twice the + * value given. + *

    + * The removed list will overflow to disk in order to ensure that deletions are + * not lost. + * + * @param maxCacheSize + */ + public void setMaxCacheSize(int maxCacheSize) + { + this.maxCacheSize = maxCacheSize; + } + + /** + * Set the name that identifies this cache from other instances. This is optional. + * + * @param name + */ + public void setName(String name) + { + this.name = name; + } + + /** + * Ensures that all properties have been set + */ + public void afterPropertiesSet() throws Exception + { + Assert.notNull(name, "name property not set"); + Assert.notNull(cacheManager, "cacheManager property not set"); + // generate the resource binding key + resourceKeyTxnData = RESOURCE_KEY_TXN_DATA + "." + name; + } + + /** + * To be used in a transaction only. + */ + private TransactionData getTransactionData() + { + TransactionData data = (TransactionData) AlfrescoTransactionSupport.getResource(resourceKeyTxnData); + if (data == null) + { + String txnId = AlfrescoTransactionSupport.getTransactionId(); + data = new TransactionData(); + // create and initialize caches + data.updatedItemsCache = new Cache( + name + "_"+ txnId + "_updates", + maxCacheSize, false, true, 0, 0); + data.removedItemsCache = new Cache( + name + "_" + txnId + "_removes", + maxCacheSize, false, true, 0, 0); + try + { + cacheManager.addCache(data.updatedItemsCache); + cacheManager.addCache(data.removedItemsCache); + } + catch (CacheException e) + { + throw new AlfrescoRuntimeException("Failed to add txn caches to manager", e); + } + AlfrescoTransactionSupport.bindResource(resourceKeyTxnData, data); + } + return data; + } + + /** + * Checks the transactional removed and updated caches before checking the shared cache. + */ + public boolean contains(K key) + { + Object value = get(key); + if (value == null) + { + return false; + } + else + { + return true; + } + } + + /** + * Checks the per-transaction caches for the object before going to the shared cache. + * If the thread is not in a transaction, then the shared cache is accessed directly. + */ + @SuppressWarnings("unchecked") + public V get(K key) + { + boolean ignoreSharedCache = false; + // are we in a transaction? + if (AlfrescoTransactionSupport.getTransactionId() != null) + { + TransactionData txnData = getTransactionData(); + try + { + if (!txnData.isClearOn) // deletions cache is still reliable + { + // check to see if the key is present in the transaction's removed items + if (txnData.removedItemsCache.get(key) != null) + { + // it has been removed in this transaction + if (logger.isDebugEnabled()) + { + logger.debug("get returning null - item has been removed from transactional cache: \n" + + " cache: " + this + "\n" + + " key: " + key); + } + return null; + } + } + + // check for the item in the transaction's new/updated items + Element element = txnData.updatedItemsCache.get(key); + if (element != null) + { + // element was found in transaction-specific updates/additions + if (logger.isDebugEnabled()) + { + logger.debug("Found item in transactional cache: \n" + + " cache: " + this + "\n" + + " key: " + key + "\n" + + " value: " + element.getValue()); + } + return (V) element.getValue(); + } + } + catch (CacheException e) + { + throw new AlfrescoRuntimeException("Cache failure", e); + } + // check if the cleared flag has been set - cleared flag means ignore shared as unreliable + ignoreSharedCache = txnData.isClearOn; + } + // no value found - must we ignore the shared cache? + if (!ignoreSharedCache) + { + // go to the shared cache + if (logger.isDebugEnabled()) + { + logger.debug("No value found in transaction - fetching instance from shared cache: \n" + + " cache: " + this + "\n" + + " key: " + key + "\n" + + " value: " + sharedCache.get(key)); + } + return (V) sharedCache.get(key); + } + else // ignore shared cache + { + if (logger.isDebugEnabled()) + { + logger.debug("No value found in transaction and ignoring shared cache: \n" + + " cache: " + this + "\n" + + " key: " + key); + } + return null; + } + } + + /** + * Goes direct to the shared cache in the absence of a transaction. + *

    + * Where a transaction is present, a cache of updated items is lazily added to the + * thread and the Object put onto that. + */ + public void put(K key, V value) + { + // are we in a transaction? + if (AlfrescoTransactionSupport.getTransactionId() == null) // not in transaction + { + // no transaction + sharedCache.put(key, value); + // done + if (logger.isDebugEnabled()) + { + logger.debug("No transaction - adding item direct to shared cache: \n" + + " cache: " + this + "\n" + + " key: " + key + "\n" + + " value: " + value); + } + } + else // transaction present + { + TransactionData txnData = getTransactionData(); + // register for callbacks + if (!txnData.listenerBound) + { + AlfrescoTransactionSupport.bindListener(this); + txnData.listenerBound = true; + } + // we have a transaction - add the item into the updated cache for this transaction + // are we in an overflow condition? + if (txnData.updatedItemsCache.getMemoryStoreSize() >= maxCacheSize) + { + // overflow about to occur or has occured - we can only guarantee non-stale + // data by clearing the shared cache after the transaction. Also, the + // shared cache needs to be ignored for the rest of the transaction. + txnData.isClearOn = true; + } + Element element = new Element(key, value); + txnData.updatedItemsCache.put(element); + // remove the item from the removed cache, if present + txnData.removedItemsCache.remove(key); + // done + if (logger.isDebugEnabled()) + { + logger.debug("In transaction - adding item direct to transactional update cache: \n" + + " cache: " + this + "\n" + + " key: " + key + "\n" + + " value: " + value); + } + } + } + + /** + * Goes direct to the shared cache in the absence of a transaction. + *

    + * Where a transaction is present, a cache of removed items is lazily added to the + * thread and the Object put onto that. + */ + public void remove(K key) + { + // are we in a transaction? + if (AlfrescoTransactionSupport.getTransactionId() == null) // not in transaction + { + // no transaction + sharedCache.remove(key); + // done + if (logger.isDebugEnabled()) + { + logger.debug("No transaction - removing item from shared cache: \n" + + " cache: " + this + "\n" + + " key: " + key); + } + } + else // transaction present + { + TransactionData txnData = getTransactionData(); + // register for callbacks + if (!txnData.listenerBound) + { + AlfrescoTransactionSupport.bindListener(this); + txnData.listenerBound = true; + } + // is the shared cache going to be cleared? + if (txnData.isClearOn) + { + // don't store removals + } + else + { + // are we in an overflow condition? + if (txnData.removedItemsCache.getMemoryStoreSize() >= maxCacheSize) + { + // overflow about to occur or has occured - we can only guarantee non-stale + // data by clearing the shared cache after the transaction. Also, the + // shared cache needs to be ignored for the rest of the transaction. + txnData.isClearOn = true; + if (logger.isDebugEnabled()) + { + logger.debug("In transaction - removal cache reach capacity reached: \n" + + " cache: " + this + "\n" + + " txn: " + AlfrescoTransactionSupport.getTransactionId()); + } + } + else + { + // add it from the removed cache for this txn + Element element = new Element(key, VALUE_DELETE); + txnData.removedItemsCache.put(element); + } + } + // remove the item from the udpated cache, if present + txnData.updatedItemsCache.remove(key); + // done + if (logger.isDebugEnabled()) + { + logger.debug("In transaction - adding item direct to transactional removed cache: \n" + + " cache: " + this + "\n" + + " key: " + key); + } + } + } + + /** + * Clears out all the caches. + */ + public void clear() + { + // clear local caches + if (AlfrescoTransactionSupport.getTransactionId() != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("In transaction clearing cache: \n" + + " cache: " + this + "\n" + + " txn: " + AlfrescoTransactionSupport.getTransactionId()); + } + + TransactionData txnData = getTransactionData(); + // register for callbacks + if (!txnData.listenerBound) + { + AlfrescoTransactionSupport.bindListener(this); + txnData.listenerBound = true; + } + // the shared cache must be cleared at the end of the transaction + // and also serves to ensure that the shared cache will be ignored + // for the remainder of the transaction + txnData.isClearOn = true; + try + { + txnData.updatedItemsCache.removeAll(); + txnData.removedItemsCache.removeAll(); + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to clear caches", e); + } + } + else // no transaction + { + if (logger.isDebugEnabled()) + { + logger.debug("No transaction - clearing shared cache"); + } + // clear shared cache + sharedCache.clear(); + } + } + + /** + * NO-OP + */ + public void flush() + { + } + + public void beforeCommit(boolean readOnly) + { + } + + public void beforeCompletion() + { + } + + /** + * Merge the transactional caches into the shared cache + */ + @SuppressWarnings("unchecked") + public void afterCommit() + { + if (logger.isDebugEnabled()) + { + logger.debug("Processing end of transaction commit"); + } + + TransactionData txnData = getTransactionData(); + + if (txnData.isClearOn) + { + // clear shared cache + sharedCache.clear(); + if (logger.isDebugEnabled()) + { + logger.debug("Clear notification recieved at end of transaction - clearing shared cache"); + } + } + else + { + // transfer any removed items + // any removed items will have also been removed from the in-transaction updates + // propogate the deletes to the shared cache + List keys = txnData.removedItemsCache.getKeys(); + for (Serializable key : keys) + { + sharedCache.remove(key); + } + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + keys.size() + " values from shared cache"); + } + } + // transfer updates + try + { + List keys = txnData.updatedItemsCache.getKeys(); + for (Serializable key : keys) + { + Element element = txnData.updatedItemsCache.get(key); + sharedCache.put(key, element.getValue()); + } + if (logger.isDebugEnabled()) + { + logger.debug("Added " + keys.size() + " values to shared cache"); + } + } + catch (CacheException e) + { + throw new AlfrescoRuntimeException("Failed to transfer updates to shared cache", e); + } + + // drop caches from cachemanager + cacheManager.removeCache(txnData.updatedItemsCache.getName()); + cacheManager.removeCache(txnData.removedItemsCache.getName()); + } + + /** + * Just allow the transactional caches to be thrown away + */ + public void afterRollback() + { + TransactionData txnData = getTransactionData(); + + // drop caches from cachemanager + cacheManager.removeCache(txnData.updatedItemsCache.getName()); + cacheManager.removeCache(txnData.removedItemsCache.getName()); + } + + /** Data holder to bind data to the transaction */ + private class TransactionData + { + public Cache updatedItemsCache; + public Cache removedItemsCache; + public boolean isClearOn; + public boolean listenerBound; + } +} diff --git a/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java new file mode 100644 index 0000000000..06cad43364 --- /dev/null +++ b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.coci; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.version.VersionableAspect; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.coci.CheckOutCheckInServiceException; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.lock.UnableToReleaseLockException; +import org.alfresco.service.cmr.repository.AspectMissingException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.QName; + +/** + * Version opertaions service implementation + * + * @author Roy Wetherall + */ +public class CheckOutCheckInServiceImpl implements CheckOutCheckInService +{ + /** + * I18N labels + */ + private static final String MSG_ERR_BAD_COPY = "coci_service.err_bad_copy"; + private static final String MSG_WORKING_COPY_LABEL = "coci_service.working_copy_label"; + private static final String MSG_ERR_NOT_OWNER = "coci_service.err_not_owner"; + private static final String MSG_ERR_ALREADY_WORKING_COPY = "coci_service.err_workingcopy_checkout"; + private static final String MSG_ERR_NOT_AUTHENTICATED = "coci_service.err_not_authenticated"; + private static final String MSG_ERR_WORKINGCOPY_HAS_NO_MIMETYPE = "coci_service.err_workingcopy_has_no_mimetype"; + + /** + * Extension character, used to recalculate the working copy names + */ + private static final String EXTENSION_CHARACTER = "."; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The version service + */ + private VersionService versionService; + + /** + * The lock service + */ + private LockService lockService; + + /** + * The copy service + */ + private CopyService copyService; + + /** + * The search service + */ + private SearchService searchService; + + /** + * The authentication service + */ + private AuthenticationService authenticationService; + + /** + * The versionable aspect behaviour implementation + */ + private VersionableAspect versionableAspect; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the version service + * + * @param versionService the version service + */ + public void setVersionService(VersionService versionService) + { + this.versionService = versionService; + } + + /** + * Sets the lock service + * + * @param lockService the lock service + */ + public void setLockService(LockService lockService) + { + this.lockService = lockService; + } + + /** + * Sets the copy service + * + * @param copyService the copy service + */ + public void setCopyService( + CopyService copyService) + { + this.copyService = copyService; + } + + /** + * Sets the authenticatin service + * + * @param authenticationService the authentication service + */ + public void setAuthenticationService( + AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * Set the search service + * + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Sets the versionable aspect behaviour implementation + * + * @param versionableAspect the versionable aspect behaviour implementation + */ + public void setVersionableAspect(VersionableAspect versionableAspect) + { + this.versionableAspect = versionableAspect; + } + + /** + * Get the working copy label. + * + * @return the working copy label + */ + public String getWorkingCopyLabel() + { + return I18NUtil.getMessage(MSG_WORKING_COPY_LABEL); + } + + /** + * @see org.alfresco.service.cmr.coci.CheckOutCheckInService#checkout(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName) + */ + public NodeRef checkout( + NodeRef nodeRef, + NodeRef destinationParentNodeRef, + QName destinationAssocTypeQName, + QName destinationAssocQName) + { + // Make sure we are no checking out a working copy node + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY) == true) + { + throw new CheckOutCheckInServiceException(MSG_ERR_ALREADY_WORKING_COPY); + } + + // Apply the lock aspect if required + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE) == false) + { + this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_LOCKABLE, null); + } + + // Rename the working copy + String copyName = (String)this.nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + if (this.getWorkingCopyLabel() != null && this.getWorkingCopyLabel().length() != 0) + { + if (copyName != null && copyName.length() != 0) + { + int index = copyName.lastIndexOf(EXTENSION_CHARACTER); + if (index > 0) + { + // Insert the working copy label before the file extension + copyName = copyName.substring(0, index) + " " + getWorkingCopyLabel() + copyName.substring(index); + } + else + { + // Simply append the working copy label onto the end of the existing name + copyName = copyName + " " + getWorkingCopyLabel(); + } + } + else + { + copyName = getWorkingCopyLabel(); + } + } + + // Make the working copy + destinationAssocQName = QName.createQName(destinationAssocQName.getNamespaceURI(), QName.createValidLocalName(copyName)); + NodeRef workingCopy = this.copyService.copy( + nodeRef, + destinationParentNodeRef, + destinationAssocTypeQName, + destinationAssocQName); + + // Update the working copy name + this.nodeService.setProperty(workingCopy, ContentModel.PROP_NAME, copyName); + + // Get the user + String userName = getUserName(); + + // Apply the working copy aspect to the working copy + Map workingCopyProperties = new HashMap(1); + workingCopyProperties.put(ContentModel.PROP_WORKING_COPY_OWNER, userName); + this.nodeService.addAspect(workingCopy, ContentModel.ASPECT_WORKING_COPY, workingCopyProperties); + + // Lock the origional node + this.lockService.lock(nodeRef, LockType.READ_ONLY_LOCK); + + // Return the working copy + return workingCopy; + } + + /** + * Gets the authenticated users node reference + * + * @return the users node reference + */ + private String getUserName() + { + String un = this.authenticationService.getCurrentUserName(); + if (un != null) + { + return un; + } + else + { + throw new CheckOutCheckInServiceException(MSG_ERR_NOT_AUTHENTICATED); + } + } + + /** + * @see org.alfresco.service.cmr.coci.CheckOutCheckInService#checkout(org.alfresco.service.cmr.repository.NodeRef) + */ + public NodeRef checkout(NodeRef nodeRef) + { + // Find the primary parent in order to determine where to put the copy + ChildAssociationRef childAssocRef = this.nodeService.getPrimaryParent(nodeRef); + + // Checkout the working copy to the same destination + return checkout(nodeRef, childAssocRef.getParentRef(), childAssocRef.getTypeQName(), childAssocRef.getQName()); + } + + /** + * @see org.alfresco.repo.version.operations.VersionOperationsService#checkin(org.alfresco.repo.ref.NodeRef, Map, java.lang.String, boolean) + */ + public NodeRef checkin( + NodeRef workingCopyNodeRef, + Map versionProperties, + String contentUrl, + boolean keepCheckedOut) + { + NodeRef nodeRef = null; + + // Check that we have been handed a working copy + if (this.nodeService.hasAspect(workingCopyNodeRef, ContentModel.ASPECT_WORKING_COPY) == false) + { + // Error since we have not been passed a working copy + throw new AspectMissingException(ContentModel.ASPECT_WORKING_COPY, workingCopyNodeRef); + } + + // Check that the working node still has the copy aspect applied + if (this.nodeService.hasAspect(workingCopyNodeRef, ContentModel.ASPECT_COPIEDFROM) == true) + { + // Disable versionable behaviours since we don't want the auto version policy behaviour to execute when we check-in + this.versionableAspect.disableAutoVersion(); + try + { + Map workingCopyProperties = nodeService.getProperties(workingCopyNodeRef); + // Try and get the origional node reference + nodeRef = (NodeRef) workingCopyProperties.get(ContentModel.PROP_COPY_REFERENCE); + if(nodeRef == null) + { + // Error since the origional node can not be found + throw new CheckOutCheckInServiceException(MSG_ERR_BAD_COPY); + } + + try + { + // Release the lock + this.lockService.unlock(nodeRef); + } + catch (UnableToReleaseLockException exception) + { + throw new CheckOutCheckInServiceException(MSG_ERR_NOT_OWNER, exception); + } + + if (contentUrl != null) + { + ContentData contentData = (ContentData) workingCopyProperties.get(ContentModel.PROP_CONTENT); + if (contentData == null) + { + throw new AlfrescoRuntimeException(MSG_ERR_WORKINGCOPY_HAS_NO_MIMETYPE, new Object[]{workingCopyNodeRef}); + } + else + { + contentData = new ContentData( + contentUrl, + contentData.getMimetype(), + contentData.getSize(), + contentData.getEncoding()); + } + // Set the content url value onto the working copy + this.nodeService.setProperty( + workingCopyNodeRef, + ContentModel.PROP_CONTENT, + contentData); + } + + // Copy the contents of the working copy onto the origional + this.copyService.copy(workingCopyNodeRef, nodeRef); + + if (versionProperties != null && this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == true) + { + // Create the new version + this.versionService.createVersion(nodeRef, versionProperties); + } + + if (keepCheckedOut == false) + { + // Delete the working copy + this.nodeService.removeAspect(workingCopyNodeRef, ContentModel.ASPECT_WORKING_COPY); + this.nodeService.deleteNode(workingCopyNodeRef); + } + else + { + // Re-lock the origional node + this.lockService.lock(nodeRef, LockType.READ_ONLY_LOCK); + } + } + finally + { + this.versionableAspect.enableAutoVersion(); + } + + } + else + { + // Error since the copy aspect is missing + throw new AspectMissingException(ContentModel.ASPECT_COPIEDFROM, workingCopyNodeRef); + } + + return nodeRef; + } + + /** + * @see org.alfresco.service.cmr.coci.CheckOutCheckInService#checkin(org.alfresco.service.cmr.repository.NodeRef, Map, java.lang.String) + */ + public NodeRef checkin( + NodeRef workingCopyNodeRef, + Map versionProperties, + String contentUrl) + { + return checkin(workingCopyNodeRef, versionProperties, contentUrl, false); + } + + /** + * @see org.alfresco.service.cmr.coci.CheckOutCheckInService#checkin(org.alfresco.service.cmr.repository.NodeRef, Map) + */ + public NodeRef checkin( + NodeRef workingCopyNodeRef, + Map versionProperties) + { + return checkin(workingCopyNodeRef, versionProperties, null, false); + } + + /** + * @see org.alfresco.service.cmr.coci.CheckOutCheckInService#cancelCheckout(org.alfresco.service.cmr.repository.NodeRef) + */ + public NodeRef cancelCheckout(NodeRef workingCopyNodeRef) + { + NodeRef nodeRef = null; + + // Check that we have been handed a working copy + if (this.nodeService.hasAspect(workingCopyNodeRef, ContentModel.ASPECT_WORKING_COPY) == false) + { + // Error since we have not been passed a working copy + throw new AspectMissingException(ContentModel.ASPECT_WORKING_COPY, workingCopyNodeRef); + } + + // Ensure that the node has the copy aspect + if (this.nodeService.hasAspect(workingCopyNodeRef, ContentModel.ASPECT_COPIEDFROM) == true) + { + // Get the origional node + nodeRef = (NodeRef)this.nodeService.getProperty(workingCopyNodeRef, ContentModel.PROP_COPY_REFERENCE); + if (nodeRef == null) + { + // Error since the origional node can not be found + throw new CheckOutCheckInServiceException(MSG_ERR_BAD_COPY); + } + + // Release the lock on the origional node + this.lockService.unlock(nodeRef); + + // Delete the working copy + this.nodeService.removeAspect(workingCopyNodeRef, ContentModel.ASPECT_WORKING_COPY); + this.nodeService.deleteNode(workingCopyNodeRef); + } + else + { + // Error since the copy aspect is missing + throw new AspectMissingException(ContentModel.ASPECT_COPIEDFROM, workingCopyNodeRef); + } + + return nodeRef; + } + + /** + * @see org.alfresco.service.cmr.coci.CheckOutCheckInService#getWorkingCopy(org.alfresco.service.cmr.repository.NodeRef) + */ + public NodeRef getWorkingCopy(NodeRef nodeRef) + { + NodeRef workingCopy = null; + + // Do a search to find the origional document + ResultSet resultSet = null; + try + { + resultSet = this.searchService.query( + nodeRef.getStoreRef(), + SearchService.LANGUAGE_LUCENE, + "ASPECT:\"" + ContentModel.ASPECT_WORKING_COPY.toString() + "\" +@\\{http\\://www.alfresco.org/model/content/1.0\\}" + ContentModel.PROP_COPY_REFERENCE.getLocalName() + ":\"" + nodeRef.toString() + "\""); + if (resultSet.getNodeRefs().size() != 0) + { + workingCopy = resultSet.getNodeRef(0); + } + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + + return workingCopy; + } +} diff --git a/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImplTest.java b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImplTest.java new file mode 100644 index 0000000000..d4d0500411 --- /dev/null +++ b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImplTest.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.coci; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; +import org.alfresco.util.TestWithUserUtils; + +/** + * Version operations service implementation unit tests + * + * @author Roy Wetherall + */ +public class CheckOutCheckInServiceImplTest extends BaseSpringTest +{ + /** + * Services used by the tests + */ + private NodeService nodeService; + private CheckOutCheckInService cociService; + private ContentService contentService; + private VersionService versionService; + private AuthenticationService authenticationService; + private LockService lockService; + private TransactionService transactionService; + private PermissionService permissionService; + + /** + * Data used by the tests + */ + private StoreRef storeRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private String userNodeRef; + + /** + * Types and properties used by the tests + */ + private static final String TEST_VALUE_NAME = "myDocument.doc"; + private static final String TEST_VALUE_2 = "testValue2"; + private static final String TEST_VALUE_3 = "testValue3"; + private static final QName PROP_NAME_QNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "name"); + private static final QName PROP2_QNAME = ContentModel.PROP_DESCRIPTION; + private static final String CONTENT_1 = "This is some content"; + private static final String CONTENT_2 = "This is the cotent modified."; + + /** + * User details + */ + //private static final String USER_NAME = "cociTest" + GUID.generate(); + private String userName; + private static final String PWD = "password"; + + /** + * On setup in transaction implementation + */ + @Override + protected void onSetUpInTransaction() + throws Exception + { + // Set the services + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.cociService = (CheckOutCheckInService)this.applicationContext.getBean("checkOutCheckInService"); + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + this.versionService = (VersionService)this.applicationContext.getBean("versionService"); + this.authenticationService = (AuthenticationService)this.applicationContext.getBean("authenticationService"); + this.lockService = (LockService)this.applicationContext.getBean("lockService"); + this.transactionService = (TransactionService)this.applicationContext.getBean("transactionComponent"); + this.permissionService = (PermissionService)this.applicationContext.getBean("permissionService"); + authenticationService.clearCurrentSecurityContext(); + + // Create the store and get the root node reference + this.storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(storeRef); + + // Create the node used for tests + ChildAssociationRef childAssocRef = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}test"), + ContentModel.TYPE_CONTENT); + this.nodeRef = childAssocRef.getChildRef(); + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_TITLED, null); + this.nodeService.setProperty(this.nodeRef, ContentModel.PROP_NAME, TEST_VALUE_NAME); + this.nodeService.setProperty(this.nodeRef, PROP2_QNAME, TEST_VALUE_2); + + // Add the initial content to the node + ContentWriter contentWriter = this.contentService.getWriter(this.nodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype("text/plain"); + contentWriter.setEncoding("UTF-8"); + contentWriter.putContent(CONTENT_1); + + // Add the lock and version aspects to the created node + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, null); + + // Create and authenticate the user + this.userName = "cociTest" + GUID.generate(); + TestWithUserUtils.createUser(this.userName, PWD, this.rootNodeRef, this.nodeService, this.authenticationService); + TestWithUserUtils.authenticateUser(this.userName, PWD, this.rootNodeRef, this.authenticationService); + this.userNodeRef = TestWithUserUtils.getCurrentUser(this.authenticationService); + + permissionService.setPermission(this.rootNodeRef, this.userName.toLowerCase(), PermissionService.ALL_PERMISSIONS, true); + permissionService.setPermission(this.nodeRef, this.userName.toLowerCase(), PermissionService.ALL_PERMISSIONS, true); + + } + + /** + * Helper method that creates a bag of properties for the test type + * + * @return bag of properties + */ + private Map createTypePropertyBag() + { + Map result = new HashMap(); + result.put(PROP_NAME_QNAME, TEST_VALUE_NAME); + return result; + } + + /** + * Test checkout + */ + public void testCheckOut() + { + checkout(); + } + + /** + * + * @return + */ + private NodeRef checkout() + { + // Check out the node + NodeRef workingCopy = this.cociService.checkout( + this.nodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}workingCopy")); + assertNotNull(workingCopy); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + + // Ensure that the working copy and copy aspect has been applied + assertTrue(this.nodeService.hasAspect(workingCopy, ContentModel.ASPECT_WORKING_COPY)); + assertTrue(this.nodeService.hasAspect(workingCopy, ContentModel.ASPECT_COPIEDFROM)); + + // Check that the working copy owner has been set correctly + assertEquals(this.userNodeRef, this.nodeService.getProperty(workingCopy, ContentModel.PROP_WORKING_COPY_OWNER)); + + // Check that the working copy name has been set correctly + String workingCopyLabel = ((CheckOutCheckInServiceImpl)this.cociService).getWorkingCopyLabel(); + String workingCopyName = (String)this.nodeService.getProperty(workingCopy, PROP_NAME_QNAME); + if (workingCopyLabel == null || workingCopyLabel.length() == 0) + { + assertEquals("myDocument.doc", workingCopyName); + } + else + { + assertEquals( + "myDocument " + workingCopyLabel + ".doc", + workingCopyName); + } + + // Ensure that the content has been copied correctly + ContentReader contentReader = this.contentService.getReader(this.nodeRef, ContentModel.PROP_CONTENT); + assertNotNull(contentReader); + ContentReader contentReader2 = this.contentService.getReader(workingCopy, ContentModel.PROP_CONTENT); + assertNotNull(contentReader2); + assertEquals( + "The content string of the working copy should match the original immediatly after checkout.", + contentReader.getContentString(), + contentReader2.getContentString()); + + return workingCopy; + } + + /** + * Test checkIn + */ + public void testCheckIn() + { + NodeRef workingCopy = checkout(); + + // Test standard check-in + Map versionProperties = new HashMap(); + versionProperties.put(Version.PROP_DESCRIPTION, "This is a test version"); + this.cociService.checkin(workingCopy, versionProperties); + + // Test check-in with content + NodeRef workingCopy3 = checkout(); + + this.nodeService.setProperty(workingCopy3, PROP_NAME_QNAME, TEST_VALUE_2); + this.nodeService.setProperty(workingCopy3, PROP2_QNAME, TEST_VALUE_3); + ContentWriter tempWriter = this.contentService.getWriter(workingCopy3, ContentModel.PROP_CONTENT, false); + assertNotNull(tempWriter); + tempWriter.putContent(CONTENT_2); + String contentUrl = tempWriter.getContentUrl(); + Map versionProperties3 = new HashMap(); + versionProperties3.put(Version.PROP_DESCRIPTION, "description"); + versionProperties3.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR); + NodeRef origNodeRef = this.cociService.checkin(workingCopy3, versionProperties3, contentUrl, true); + assertNotNull(origNodeRef); + + // Check the checked in content + ContentReader contentReader = this.contentService.getReader(origNodeRef, ContentModel.PROP_CONTENT); + assertNotNull(contentReader); + assertEquals(CONTENT_2, contentReader.getContentString()); + + // Check that the version history is correct + Version version = this.versionService.getCurrentVersion(origNodeRef); + assertNotNull(version); + assertEquals("description", version.getDescription()); + assertEquals(VersionType.MAJOR, version.getVersionType()); + NodeRef versionNodeRef = version.getFrozenStateNodeRef(); + assertNotNull(versionNodeRef); + + // Check the verioned content + ContentReader versionContentReader = this.contentService.getReader(versionNodeRef, ContentModel.PROP_CONTENT); + assertNotNull(versionContentReader); + assertEquals(CONTENT_2, versionContentReader.getContentString()); + + // Check that the name is not updated during the check-in + assertEquals(TEST_VALUE_NAME, this.nodeService.getProperty(versionNodeRef, PROP_NAME_QNAME)); + assertEquals(TEST_VALUE_NAME, this.nodeService.getProperty(origNodeRef, PROP_NAME_QNAME)); + + // Check that the other properties are updated during the check-in + assertEquals(TEST_VALUE_3, this.nodeService.getProperty(versionNodeRef, PROP2_QNAME)); + assertEquals(TEST_VALUE_3, this.nodeService.getProperty(origNodeRef, PROP2_QNAME)); + + // Cancel the check out after is has been left checked out + this.cociService.cancelCheckout(workingCopy3); + + // Test keep checked out flag + NodeRef workingCopy2 = checkout(); + Map versionProperties2 = new HashMap(); + versionProperties2.put(Version.PROP_DESCRIPTION, "Another version test"); + this.cociService.checkin(workingCopy2, versionProperties2, null, true); + this.cociService.checkin(workingCopy2, new HashMap(), null, true); + } + + /** + * Test when the aspect is not set when check-in is performed + */ + public void testVersionAspectNotSetOnCheckIn() + { + // Create a bag of props + Map bagOfProps = createTypePropertyBag(); + bagOfProps.put(ContentModel.PROP_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, "UTF-8")); + + // Create a new node + ChildAssociationRef childAssocRef = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}test"), + ContentModel.TYPE_CONTENT, + bagOfProps); + NodeRef noVersionNodeRef = childAssocRef.getChildRef(); + + // Check out and check in + NodeRef workingCopy = this.cociService.checkout(noVersionNodeRef); + this.cociService.checkin(workingCopy, new HashMap()); + + // Check that the origional node has no version history dispite sending verion props + assertNull(this.versionService.getVersionHistory(noVersionNodeRef)); + } + + /** + * Test cancel checkOut + */ + public void testCancelCheckOut() + { + NodeRef workingCopy = checkout(); + assertNotNull(workingCopy); + + try + { + this.lockService.checkForLock(this.nodeRef); + fail("The origional should be locked now."); + } + catch (Throwable exception) + { + // Good the origional is locked + } + + NodeRef origNodeRef = this.cociService.cancelCheckout(workingCopy); + assertEquals(this.nodeRef, origNodeRef); + + // The origional should no longer be locked + this.lockService.checkForLock(origNodeRef); + } + + /** + * Test the deleting a wokring copy node removed the lock on the origional node + */ + public void testAutoCancelCheckOut() + { + NodeRef workingCopy = checkout(); + assertNotNull(workingCopy); + + try + { + this.lockService.checkForLock(this.nodeRef); + fail("The origional should be locked now."); + } + catch (Throwable exception) + { + // Good the origional is locked + } + + // Delete the working copy + this.nodeService.deleteNode(workingCopy); + + // The origional should no longer be locked + this.lockService.checkForLock(this.nodeRef); + + } + + /** + * Test the getWorkingCopy method + */ + public void testGetWorkingCopy() + { + NodeRef origNodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}test2"), + ContentModel.TYPE_CONTENT).getChildRef(); + + + NodeRef wk1 = this.cociService.getWorkingCopy(origNodeRef); + assertNull(wk1); + + // Check the document out + final NodeRef workingCopy = this.cociService.checkout(origNodeRef); + + // Need to commit the transaction in order to get the indexer to run + setComplete(); + endTransaction(); + + final NodeRef finalNodeRef = origNodeRef; + + TransactionUtil.executeInUserTransaction( + this.transactionService, + new TransactionUtil.TransactionWork() + { + public Object doWork() + { + NodeRef wk2 = CheckOutCheckInServiceImplTest.this.cociService.getWorkingCopy(finalNodeRef); + assertNotNull(wk2); + assertEquals(workingCopy, wk2); + + CheckOutCheckInServiceImplTest.this.cociService.cancelCheckout(workingCopy); + return null; + } + + }); + + //NodeRef wk3 = this.cociService.getWorkingCopy(this.nodeRef); + //assertNull(wk3); + } + +} diff --git a/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java b/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java new file mode 100644 index 0000000000..c60b242e2e --- /dev/null +++ b/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +package org.alfresco.repo.coci; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +public class WorkingCopyAspect +{ + /** + * Policy component + */ + private PolicyComponent policyComponent; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The lock service + */ + private LockService lockService; + + /** + * Sets the policy component + * + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the lock service + * + * @param lockService the lock service + */ + public void setLockService(LockService lockService) + { + this.lockService = lockService; + } + + /** + * Initialise method + */ + public void init() + { + // Register copy behaviour for the working copy aspect + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + ContentModel.ASPECT_WORKING_COPY, + new JavaBehaviour(this, "onCopy")); + + // register onBeforeDelete class behaviour for the working copy aspect + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), + ContentModel.ASPECT_WORKING_COPY, + new JavaBehaviour(this, "beforeDeleteNode")); + } + + /** + * onCopy policy behaviour + * + * @see org.alfresco.repo.copy.CopyServicePolicies.OnCopyNodePolicy#onCopyNode(QName, NodeRef, StoreRef, boolean, PolicyScope) + */ + public void onCopy( + QName sourceClassRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + if (copyToNewNode == false) + { + // Make sure that the name of the node is not updated with the working copy name + copyDetails.removeProperty(ContentModel.PROP_NAME); + } + + // NOTE: the working copy aspect is not added since it should not be copyied + } + + /** + * beforeDeleteNode policy behaviour + * + * @param nodeRef the node reference about to be deleted + */ + public void beforeDeleteNode(NodeRef nodeRef) + { + // Prior to deleting a working copy the lock on the origional node should be released + // Note: we do not call cancelCheckOut since this will also attempt to delete the node is question + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY) == true && + this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_COPIEDFROM) == true) + { + // Get the origional node + NodeRef origNodeRef = (NodeRef)this.nodeService.getProperty(nodeRef, ContentModel.PROP_COPY_REFERENCE); + if (origNodeRef != null) + { + // Release the lock on the origional node + this.lockService.unlock(origNodeRef); + } + } + } + +} diff --git a/source/java/org/alfresco/repo/configuration/ConfigurableService.java b/source/java/org/alfresco/repo/configuration/ConfigurableService.java new file mode 100644 index 0000000000..cc9a971f59 --- /dev/null +++ b/source/java/org/alfresco/repo/configuration/ConfigurableService.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.configuration; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Configurable service interface + * + * @author Roy Wetherall + */ +public interface ConfigurableService +{ + + + /** + * Indicates whether a node is configurable or not + * + * @param nodeRef the node reference + * @return true if the node is configurable, false otherwise + */ + public boolean isConfigurable(NodeRef nodeRef); + + /** + * Makes a specified node Configurable. + *

    + * This will create the cofigurable folder, associate it as a child of the node and apply the + * configurable aspect to the node. + * + * @param nodeRef the node reference + */ + public void makeConfigurable(NodeRef nodeRef); + + /** + * Get the configuration folder associated with a configuration node + * + * @param nodeRef the node reference + * @return the configuration folder + */ + public NodeRef getConfigurationFolder(NodeRef nodeRef); + +} diff --git a/source/java/org/alfresco/repo/configuration/ConfigurableServiceImpl.java b/source/java/org/alfresco/repo/configuration/ConfigurableServiceImpl.java new file mode 100644 index 0000000000..9096928ab6 --- /dev/null +++ b/source/java/org/alfresco/repo/configuration/ConfigurableServiceImpl.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.configuration; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * @author Roy Wetherall + */ +public class ConfigurableServiceImpl implements ConfigurableService +{ + private NodeService nodeService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public boolean isConfigurable(NodeRef nodeRef) + { + return this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_CONFIGURABLE); + } + + public void makeConfigurable(NodeRef nodeRef) + { + if (isConfigurable(nodeRef) == false) + { + // First apply the aspect + this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_CONFIGURABLE, null); + + // Next create and add the configurations folder + this.nodeService.createNode( + nodeRef, + ContentModel.ASSOC_CONFIGURATIONS, + ContentModel.ASSOC_CONFIGURATIONS, + ContentModel.TYPE_CONFIGURATIONS); + } + } + + public NodeRef getConfigurationFolder(NodeRef nodeRef) + { + NodeRef result = null; + if (isConfigurable(nodeRef) == true) + { + List assocs = this.nodeService.getChildAssocs( + nodeRef, + RegexQNamePattern.MATCH_ALL, + ContentModel.ASSOC_CONFIGURATIONS); + if (assocs.size() != 0) + { + ChildAssociationRef assoc = assocs.get(0); + result = assoc.getChildRef(); + } + } + return result; + } + +} diff --git a/source/java/org/alfresco/repo/configuration/ConfigurableServiceImplTest.java b/source/java/org/alfresco/repo/configuration/ConfigurableServiceImplTest.java new file mode 100644 index 0000000000..c1b81e36ed --- /dev/null +++ b/source/java/org/alfresco/repo/configuration/ConfigurableServiceImplTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.configuration; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.BaseSpringTest; + +/** + * Configurable service implementation test + * + * @author Roy Wetherall + */ +public class ConfigurableServiceImplTest extends BaseSpringTest +{ + public NodeService nodeService; + private ServiceRegistry serviceRegistry; + private ConfigurableService configurableService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + + /** + * onSetUpInTransaction + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.serviceRegistry = (ServiceRegistry)this.applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + this.configurableService = (ConfigurableService)this.applicationContext.getBean("configurableService"); + + this.testStoreRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + } + + /** + * Test isConfigurable + */ + public void testIsConfigurable() + { + assertFalse(this.configurableService.isConfigurable(this.nodeRef)); + this.configurableService.makeConfigurable(this.nodeRef); + assertTrue(this.configurableService.isConfigurable(this.nodeRef)); + } + + /** + * Test make configurable + */ + public void testMakeConfigurable() + { + this.configurableService.makeConfigurable(this.nodeRef); + assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_CONFIGURABLE)); + List assocs = this.nodeService.getChildAssocs( + this.nodeRef, + RegexQNamePattern.MATCH_ALL, + ContentModel.ASSOC_CONFIGURATIONS); + assertNotNull(assocs); + assertEquals(1, assocs.size()); + } + + /** + * Test getConfigurationFolder + */ + public void testGetConfigurationFolder() + { + assertNull(this.configurableService.getConfigurationFolder(this.nodeRef)); + this.configurableService.makeConfigurable(nodeRef); + NodeRef configFolder = this.configurableService.getConfigurationFolder(this.nodeRef); + assertNotNull(configFolder); + NodeRef parentNodeRef = this.nodeService.getPrimaryParent(configFolder).getParentRef(); + assertNotNull(parentNodeRef); + assertEquals(nodeRef, parentNodeRef); + } + +} diff --git a/source/java/org/alfresco/repo/content/AbstractContentAccessor.java b/source/java/org/alfresco/repo/content/AbstractContentAccessor.java new file mode 100644 index 0000000000..1f7d1ef207 --- /dev/null +++ b/source/java/org/alfresco/repo/content/AbstractContentAccessor.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.repository.ContentAccessor; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.AfterReturningAdvice; + +/** + * Provides basic support for content accessors. + * + * @author Derek Hulley + */ +public abstract class AbstractContentAccessor implements ContentAccessor +{ + private static Log logger = LogFactory.getLog(AbstractContentAccessor.class); + + /** when set, ensures that listeners are executed within a transaction */ + private TransactionService transactionService; + + private String contentUrl; + private String mimetype; + private String encoding; + + /** + * @param contentUrl the content URL + */ + protected AbstractContentAccessor(String contentUrl) + { + if (contentUrl == null || contentUrl.length() == 0) + { + throw new IllegalArgumentException("contentUrl must be a valid String"); + } + this.contentUrl = contentUrl; + + // the default encoding is Java's default encoding + encoding = "UTF-8"; + } + + public ContentData getContentData() + { + ContentData property = new ContentData(contentUrl, mimetype, getSize(), encoding); + return property; + } + + /** + * Provides access to transactions for implementing classes + * + * @return Returns a source of user transactions + */ + protected TransactionService getTransactionService() + { + return transactionService; + } + + /** + * Set the transaction provider to be used by {@link ContentStreamListener listeners}. + * + * @param transactionService the transaction service to wrap callback code in + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(100); + sb.append("ContentAccessor") + .append("[ contentUrl=").append(getContentUrl()) + .append(", mimetype=").append(getMimetype()) + .append(", size=").append(getSize()) + .append(", encoding=").append(getEncoding()) + .append("]"); + return sb.toString(); + } + + public String getContentUrl() + { + return contentUrl; + } + + public String getMimetype() + { + return mimetype; + } + + /** + * @param mimetype the underlying content's mimetype - null if unknown + */ + public void setMimetype(String mimetype) + { + this.mimetype = mimetype; + } + + /** + * @return Returns the content encoding - null if unknown + */ + public String getEncoding() + { + return encoding; + } + + /** + * @param encoding the underlying content's encoding - null if unknown + */ + public void setEncoding(String encoding) + { + this.encoding = encoding; + } + + /** + * Advise that listens for the completion of specific methods on the + * {@link java.nio.channels.ByteChannel} interface. + * + * @author Derek Hulley + */ + protected class ByteChannelCallbackAdvise implements AfterReturningAdvice + { + private List listeners; + + public ByteChannelCallbackAdvise(List listeners) + { + this.listeners = listeners; + } + + /** + * Provides transactional callbacks to the listeners + */ + public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable + { + // check for specific events + if (method.getName().equals("close")) + { + fireChannelClosed(); + } + } + + private void fireChannelClosed() + { + if (listeners.size() == 0) + { + // nothing to do + return; + } + // ensure that we are in a transaction + if (transactionService == null) + { + throw new AlfrescoRuntimeException("A transaction service is required when there are listeners present"); + } + TransactionUtil.TransactionWork work = new TransactionUtil.TransactionWork() + { + public Object doWork() + { + // call the listeners + for (ContentStreamListener listener : listeners) + { + listener.contentStreamClosed(); + } + return null; + } + }; + TransactionUtil.executeInUserTransaction(transactionService, work); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Content listeners called: close"); + } + } + } + + /** + * Wraps a FileChannel to provide callbacks to listeners when the + * channel is {@link java.nio.channels.Channel#close() closed}. + * + * @author Derek Hulley + */ + protected class CallbackFileChannel extends FileChannel + { + /** the channel to route all calls to */ + private FileChannel delegate; + /** listeners waiting for the stream close */ + private List listeners; + + /** + * @param delegate the channel that will perform the work + * @param listeners listeners for events coming from this channel + */ + public CallbackFileChannel( + FileChannel delegate, + List listeners) + { + if (delegate == null) + { + throw new IllegalArgumentException("FileChannel delegate is required"); + } + if (delegate instanceof CallbackFileChannel) + { + throw new IllegalArgumentException("FileChannel delegate may not be a CallbackFileChannel"); + } + + this.delegate = delegate; + this.listeners = listeners; + } + + /** + * Closes the channel and makes the callbacks to the listeners + */ + @Override + protected void implCloseChannel() throws IOException + { + delegate.close(); + fireChannelClosed(); + } + + /** + * Helper method to notify stream listeners + */ + private void fireChannelClosed() + { + if (listeners.size() == 0) + { + // nothing to do + return; + } + // create the work to update the listeners + TransactionUtil.TransactionWork work = new TransactionUtil.TransactionWork() + { + public Object doWork() + { + // call the listeners + for (ContentStreamListener listener : listeners) + { + listener.contentStreamClosed(); + } + return null; + } + }; + TransactionUtil.executeInUserTransaction(transactionService, work); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Content listeners called: close"); + } + } + + @Override + public void force(boolean metaData) throws IOException + { + delegate.force(metaData); + } + + @Override + public FileLock lock(long position, long size, boolean shared) throws IOException + { + return delegate.lock(position, size, shared); + } + + @Override + public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException + { + return delegate.map(mode, position, size); + } + + @Override + public long position() throws IOException + { + return delegate.position(); + } + + @Override + public FileChannel position(long newPosition) throws IOException + { + return delegate.position(newPosition); + } + + @Override + public int read(ByteBuffer dst) throws IOException + { + return delegate.read(dst); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException + { + return delegate.read(dst, position); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException + { + return delegate.read(dsts, offset, length); + } + + @Override + public long size() throws IOException + { + return delegate.size(); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException + { + return delegate.transferFrom(src, position, count); + } + + @Override + public long transferTo(long position, long count, WritableByteChannel target) throws IOException + { + return delegate.transferTo(position, count, target); + } + + @Override + public FileChannel truncate(long size) throws IOException + { + return delegate.truncate(size); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) throws IOException + { + return delegate.tryLock(position, size, shared); + } + + @Override + public int write(ByteBuffer src) throws IOException + { + return delegate.write(src); + } + + @Override + public int write(ByteBuffer src, long position) throws IOException + { + return delegate.write(src, position); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException + { + return delegate.write(srcs, offset, length); + } + } +} diff --git a/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java b/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java new file mode 100644 index 0000000000..ff2b17e815 --- /dev/null +++ b/source/java/org/alfresco/repo/content/AbstractContentReadWriteTest.java @@ -0,0 +1,624 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.util.Set; + +import junit.framework.TestCase; + +import org.alfresco.repo.transaction.DummyTransactionService; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.cmr.repository.ContentWriter; + +/** + * Abstract base class that provides a set of tests for implementations + * of the content readers and writers. + * + * @see org.alfresco.service.cmr.repository.ContentReader + * @see org.alfresco.service.cmr.repository.ContentWriter + * + * @author Derek Hulley + */ +public abstract class AbstractContentReadWriteTest extends TestCase +{ + private String contentUrl; + + public AbstractContentReadWriteTest() + { + super(); + } + + @Override + public void setUp() throws Exception + { + contentUrl = AbstractContentStore.createNewUrl(); + } + + /** + * Fetch the store to be used during a test. This method is invoked once per test - it is + * therefore safe to use setUp to initialise resources. + *

    + * Usually tests will construct a static instance of the store to use throughout all the + * tests. + * + * @return Returns the same instance of a store for all invocations. + */ + protected abstract ContentStore getStore(); + + /** + * @see #getStore() + */ + protected final ContentWriter getWriter() + { + return getStore().getWriter(null, contentUrl); + } + + /** + * @see #getStore() + */ + protected final ContentReader getReader() + { + return getStore().getReader(contentUrl); + } + + public void testSetUp() throws Exception + { + assertNotNull("setUp() not executed: no content URL present"); + + // check that the store remains the same + ContentStore store = getStore(); + assertNotNull("No store provided", store); + assertTrue("The same instance of the store must be returned for getStore", store == getStore()); + } + + public void testContentUrl() throws Exception + { + ContentReader reader = getReader(); + ContentWriter writer = getWriter(); + + // the contract is that both the reader and writer must refer to the same + // content -> the URL must be the same + String readerContentUrl = reader.getContentUrl(); + String writerContentUrl = writer.getContentUrl(); + assertNotNull("Reader url is invalid", readerContentUrl); + assertNotNull("Writer url is invalid", writerContentUrl); + assertEquals("Reader and writer must reference same content", + readerContentUrl, + writerContentUrl); + + // check that the content URL is correct + assertTrue("Content URL doesn't start with correct prefix", + readerContentUrl.startsWith(ContentStore.STORE_PROTOCOL)); + } + + public void testMimetypeAndEncoding() throws Exception + { + ContentWriter writer = getWriter(); + // set mimetype and encoding + writer.setMimetype("text/plain"); + writer.setEncoding("UTF-16"); + + // create a UTF-16 string + String content = "A little bit o' this and a little bit o' that"; + byte[] bytesUtf16 = content.getBytes("UTF-16"); + // write the bytes directly to the writer + OutputStream os = writer.getContentOutputStream(); + os.write(bytesUtf16); + os.close(); + + // now get a reader from the writer + ContentReader reader = writer.getReader(); + assertEquals("Writer -> Reader content URL mismatch", writer.getContentUrl(), reader.getContentUrl()); + assertEquals("Writer -> Reader mimetype mismatch", writer.getMimetype(), reader.getMimetype()); + assertEquals("Writer -> Reader encoding mismatch", writer.getEncoding(), reader.getEncoding()); + + // now get the string directly from the reader + String contentCheck = reader.getContentString(); // internally it should have taken care of the encoding + assertEquals("Encoding and decoding of strings failed", content, contentCheck); + } + + public void testExists() throws Exception + { + ContentStore store = getStore(); + + // make up a URL + String contentUrl = AbstractContentStore.createNewUrl(); + + // it should not exist in the store + assertFalse("Store exists fails with new URL", store.exists(contentUrl)); + + // get a reader + ContentReader reader = store.getReader(contentUrl); + assertNotNull("Reader must be present, even for missing content", reader); + assertFalse("Reader exists failure", reader.exists()); + + // write something + ContentWriter writer = store.getWriter(null, contentUrl); + writer.putContent("ABC"); + + assertTrue("Store exists should show URL to be present", store.exists(contentUrl)); + } + + public void testGetReader() throws Exception + { + ContentWriter writer = getWriter(); + + // check that no reader is available from the writer just yet + ContentReader nullReader = writer.getReader(); + assertNull("No reader expected", nullReader); + + String content = "ABC"; + // write some content + long before = System.currentTimeMillis(); + writer.setMimetype("text/plain"); + writer.setEncoding("UTF-8"); + writer.putContent(content); + long after = System.currentTimeMillis(); + + // get a reader from the writer + ContentReader readerFromWriter = writer.getReader(); + assertEquals("URL incorrect", writer.getContentUrl(), readerFromWriter.getContentUrl()); + assertEquals("Mimetype incorrect", writer.getMimetype(), readerFromWriter.getMimetype()); + assertEquals("Encoding incorrect", writer.getEncoding(), readerFromWriter.getEncoding()); + + // get another reader from the reader + ContentReader readerFromReader = readerFromWriter.getReader(); + assertEquals("URL incorrect", writer.getContentUrl(), readerFromReader.getContentUrl()); + assertEquals("Mimetype incorrect", writer.getMimetype(), readerFromReader.getMimetype()); + assertEquals("Encoding incorrect", writer.getEncoding(), readerFromReader.getEncoding()); + + // check the content + String contentCheck = readerFromWriter.getContentString(); + assertEquals("Content is incorrect", content, contentCheck); + + // check that the length is correct + int length = content.getBytes(writer.getEncoding()).length; + assertEquals("Reader content length is incorrect", length, readerFromWriter.getSize()); + + // check that the last modified time is correct + long modifiedTimeCheck = readerFromWriter.getLastModified(); + assertTrue("Reader last modified is incorrect", before <= modifiedTimeCheck); + assertTrue("Reader last modified is incorrect", modifiedTimeCheck <= after); + } + + public void testClosedState() throws Exception + { + ContentReader reader = getReader(); + ContentWriter writer = getWriter(); + + // check that streams are not flagged as closed + assertFalse("Reader stream should not be closed", reader.isClosed()); + assertFalse("Writer stream should not be closed", writer.isClosed()); + + // check that the write doesn't supply a reader + ContentReader writerGivenReader = writer.getReader(); + assertNull("No reader should be available before a write has finished", writerGivenReader); + + // write some stuff + writer.putContent("ABC"); + // check that the write has been closed + assertTrue("Writer stream should be closed", writer.isClosed()); + + // check that we can get a reader from the writer + writerGivenReader = writer.getReader(); + assertNotNull("No reader given by closed writer", writerGivenReader); + assertFalse("Readers should still be closed", reader.isClosed()); + assertFalse("Readers should still be closed", writerGivenReader.isClosed()); + + // check that the instance is new each time + ContentReader newReaderA = writer.getReader(); + ContentReader newReaderB = writer.getReader(); + assertFalse("Reader must always be a new instance", newReaderA == newReaderB); + + // check that the readers refer to the same URL + assertEquals("Readers should refer to same URL", + reader.getContentUrl(), writerGivenReader.getContentUrl()); + + // read their content + String contentCheck = reader.getContentString(); + assertEquals("Incorrect content", "ABC", contentCheck); + contentCheck = writerGivenReader.getContentString(); + assertEquals("Incorrect content", "ABC", contentCheck); + + // check closed state of readers + assertTrue("Reader should be closed", reader.isClosed()); + assertTrue("Reader should be closed", writerGivenReader.isClosed()); + } + + /** + * Checks that the store disallows concurrent writers to be issued to the same URL. + */ + public void testConcurrentWriteDetection() throws Exception + { + String contentUrl = AbstractContentStore.createNewUrl(); + ContentStore store = getStore(); + + ContentWriter firstWriter = store.getWriter(null, contentUrl); + try + { + ContentWriter secondWriter = store.getWriter(null, contentUrl); + fail("Store issued two writers for the same URL: " + store); + } + catch (ContentIOException e) + { + // expected + } + } + + /** + * Checks that the writer can have a listener attached + */ + public void testWriteStreamListener() throws Exception + { + ContentWriter writer = getWriter(); + + final boolean[] streamClosed = new boolean[] {false}; // has to be final + ContentStreamListener listener = new ContentStreamListener() + { + public void contentStreamClosed() throws ContentIOException + { + streamClosed[0] = true; + } + }; + writer.setTransactionService(new DummyTransactionService()); + writer.addListener(listener); + + // write some content + writer.putContent("ABC"); + + // check that the listener was called + assertTrue("Write stream listener was not called for the stream close", streamClosed[0]); + } + + /** + * The simplest test. Write a string and read it again, checking that we receive the same values. + * If the resource accessed by {@link #getReader()} and {@link #getWriter()} is not the same, then + * values written and read won't be the same. + */ + public void testWriteAndReadString() throws Exception + { + ContentReader reader = getReader(); + ContentWriter writer = getWriter(); + + String content = "ABC"; + writer.putContent(content); + assertTrue("Stream close not detected", writer.isClosed()); + + String check = reader.getContentString(); + assertTrue("Read and write may not share same resource", check.length() > 0); + assertEquals("Write and read didn't work", content, check); + } + + public void testStringTruncation() throws Exception + { + String content = "1234567890"; + + ContentWriter writer = getWriter(); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); // shorter format i.t.o. bytes used + // write the content + writer.putContent(content); + + // get a reader - get it in a larger format i.t.o. bytes + ContentReader reader = writer.getReader(); + String checkContent = reader.getContentString(5); + assertEquals("Truncated strings don't match", "12345", checkContent); + } + + public void testReadAndWriteFile() throws Exception + { + ContentReader reader = getReader(); + ContentWriter writer = getWriter(); + + File sourceFile = File.createTempFile(getName(), ".txt"); + sourceFile.deleteOnExit(); + // dump some content into the temp file + String content = "ABC"; + FileOutputStream os = new FileOutputStream(sourceFile); + os.write(content.getBytes()); + os.flush(); + os.close(); + + // put our temp file's content + writer.putContent(sourceFile); + assertTrue("Stream close not detected", writer.isClosed()); + + // create a sink temp file + File sinkFile = File.createTempFile(getName(), ".txt"); + sinkFile.deleteOnExit(); + + // get the content into our temp file + reader.getContent(sinkFile); + + // read the sink file manually + FileInputStream is = new FileInputStream(sinkFile); + byte[] buffer = new byte[100]; + int count = is.read(buffer); + assertEquals("No content read", 3, count); + is.close(); + String check = new String(buffer, 0, count); + + assertEquals("Write out of and read into files failed", content, check); + } + + public void testReadAndWriteStreamByPull() throws Exception + { + ContentReader reader = getReader(); + ContentWriter writer = getWriter(); + + String content = "ABC"; + // put the content using a stream + InputStream is = new ByteArrayInputStream(content.getBytes()); + writer.putContent(is); + assertTrue("Stream close not detected", writer.isClosed()); + + // get the content using a stream + ByteArrayOutputStream os = new ByteArrayOutputStream(100); + reader.getContent(os); + byte[] bytes = os.toByteArray(); + String check = new String(bytes); + + assertEquals("Write out and read in using streams failed", content, check); + } + + public void testReadAndWriteStreamByPush() throws Exception + { + ContentReader reader = getReader(); + ContentWriter writer = getWriter(); + + String content = "ABC"; + // get the content output stream + OutputStream os = writer.getContentOutputStream(); + os.write(content.getBytes()); + assertFalse("Stream has not been closed", writer.isClosed()); + // close the stream and check again + os.close(); + assertTrue("Stream close not detected", writer.isClosed()); + + // pull the content from a stream + InputStream is = reader.getContentInputStream(); + byte[] buffer = new byte[100]; + int count = is.read(buffer); + assertEquals("No content read", 3, count); + is.close(); + String check = new String(buffer, 0, count); + + assertEquals("Write out of and read into files failed", content, check); + } + + /** + * Tests deletion of content. + *

    + * Only applies when {@link #getStore()} returns a value. + */ + public void testDelete() throws Exception + { + ContentStore store = getStore(); + ContentWriter writer = getWriter(); + + String content = "ABC"; + String contentUrl = writer.getContentUrl(); + + // write some bytes, but don't close the stream + OutputStream os = writer.getContentOutputStream(); + os.write(content.getBytes()); + os.flush(); // make sure that the bytes get persisted + + // with the stream open, attempt to delete the content + boolean deleted = store.delete(contentUrl); + assertFalse("Should not be able to delete content with open write stream", deleted); + + // close the stream + os.close(); + + // get a reader + ContentReader reader = store.getReader(contentUrl); + assertNotNull(reader); + ContentReader readerCheck = writer.getReader(); + assertNotNull(readerCheck); + assertEquals("Store and write provided readers onto different URLs", + writer.getContentUrl(), reader.getContentUrl()); + + // open the stream onto the content + InputStream is = reader.getContentInputStream(); + + // attempt to delete the content + deleted = store.delete(contentUrl); + assertFalse("Content deletion failed to detect active reader", deleted); + + // close the reader stream + is.close(); + + // get a fresh reader + reader = store.getReader(contentUrl); + assertNotNull(reader); + assertTrue("Content should exist", reader.exists()); + // delete the content + store.delete(contentUrl); + + // attempt to read from the reader + try + { + is = reader.getContentInputStream(); + fail("Reader failed to detect underlying content deletion"); + } + catch (ContentIOException e) + { + // expected + } + + // get another fresh reader + reader = store.getReader(contentUrl); + assertNotNull("Reader must be returned even when underlying content is missing", + reader); + assertFalse("Content should not exist", reader.exists()); + try + { + is = reader.getContentInputStream(); + fail("Reader opened stream onto missing content"); + } + catch (ContentIOException e) + { + // expected + } + } + + /** + * Tests retrieval of all content URLs + *

    + * Only applies when {@link #getStore()} returns a value. + */ + public void testListUrls() throws Exception + { + ContentStore store = getStore(); + + ContentWriter writer = getWriter(); + + Set contentUrls = store.getUrls(); + String contentUrl = writer.getContentUrl(); + assertTrue("Writer URL not listed by store", contentUrls.contains(contentUrl)); + + // write some data + writer.putContent("The quick brown fox..."); + + // check again + contentUrls = store.getUrls(); + assertTrue("Writer URL not listed by store", contentUrls.contains(contentUrl)); + + // delete the content + boolean deleted = store.delete(contentUrl); + if (deleted) + { + contentUrls = store.getUrls(); + assertFalse("Successfully deleted URL still shown by store", contentUrls.contains(contentUrl)); + } + } + + /** + * Tests random access writing + *

    + * Only executes if the writer implements {@link RandomAccessContent}. + */ + public void testRandomAccessWrite() throws Exception + { + ContentWriter writer = getWriter(); + if (!(writer instanceof RandomAccessContent)) + { + // not much to do here + return; + } + RandomAccessContent randomWriter = (RandomAccessContent) writer; + // check that we are allowed to write + assertTrue("Expected random access writing", randomWriter.canWrite()); + + FileChannel fileChannel = randomWriter.getChannel(); + assertNotNull("No channel given", fileChannel); + + // check that no other content access is allowed + try + { + writer.getWritableChannel(); + fail("Second channel access allowed"); + } + catch (RuntimeException e) + { + // expected + } + + // write some content in a random fashion (reverse order) + byte[] content = new byte[] {1, 2, 3}; + for (int i = content.length - 1; i >= 0; i--) + { + ByteBuffer buffer = ByteBuffer.wrap(content, i, 1); + fileChannel.write(buffer, i); + } + + // close the channel + fileChannel.close(); + assertTrue("Writer not closed", writer.isClosed()); + + // check the content + ContentReader reader = writer.getReader(); + ReadableByteChannel channelReader = reader.getReadableChannel(); + ByteBuffer buffer = ByteBuffer.allocateDirect(3); + int count = channelReader.read(buffer); + assertEquals("Incorrect number of bytes read", 3, count); + for (int i = 0; i < content.length; i++) + { + assertEquals("Content doesn't match", content[i], buffer.get(i)); + } + } + + /** + * Tests random access reading + *

    + * Only executes if the reader implements {@link RandomAccessContent}. + */ + public void testRandomAccessRead() throws Exception + { + ContentWriter writer = getWriter(); + // put some content + String content = "ABC"; + byte[] bytes = content.getBytes(); + writer.putContent(content); + ContentReader reader = writer.getReader(); + if (!(reader instanceof RandomAccessContent)) + { + // not much to do here + return; + } + RandomAccessContent randomReader = (RandomAccessContent) reader; + // check that we are NOT allowed to write + assertFalse("Expected read-only random access", randomReader.canWrite()); + + FileChannel fileChannel = randomReader.getChannel(); + assertNotNull("No channel given", fileChannel); + + // check that no other content access is allowed + try + { + reader.getReadableChannel(); + fail("Second channel access allowed"); + } + catch (RuntimeException e) + { + // expected + } + + // read the content + ByteBuffer buffer = ByteBuffer.allocate(bytes.length); + int count = fileChannel.read(buffer); + assertEquals("Incorrect number of bytes read", bytes.length, count); + // transfer back to array + buffer.rewind(); + buffer.get(bytes); + String checkContent = new String(bytes); + assertEquals("Content read failure", content, checkContent); + } +} diff --git a/source/java/org/alfresco/repo/content/AbstractContentReader.java b/source/java/org/alfresco/repo/content/AbstractContentReader.java new file mode 100644 index 0000000000..9cdbdd733f --- /dev/null +++ b/source/java/org/alfresco/repo/content/AbstractContentReader.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentAccessor; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.util.FileCopyUtils; + +/** + * Implements all the convenience methods of the interface. The only methods + * that need to be implemented, i.e. provide low-level content access are: + *

      + *
    • {@link #getDirectReadableChannel()} to read content from the repository
    • + *
    + * + * @author Derek Hulley + */ +public abstract class AbstractContentReader extends AbstractContentAccessor implements ContentReader +{ + private static final Log logger = LogFactory.getLog(AbstractContentReader.class); + + private List listeners; + private ReadableByteChannel channel; + + /** + * @param contentUrl the content URL - this should be relative to the root of the store + * and not absolute: to enable moving of the stores + */ + protected AbstractContentReader(String contentUrl) + { + super(contentUrl); + + listeners = new ArrayList(2); + } + + /** + * Adds the listener after checking that the output stream isn't already in + * use. + */ + public synchronized void addListener(ContentStreamListener listener) + { + if (channel != null) + { + throw new RuntimeException("Channel is already in use"); + } + listeners.add(listener); + } + + /** + * A factory method for subclasses to implement that will ensure the proper + * implementation of the {@link ContentReader#getReader()} method. + *

    + * Only the instance need be constructed. The required mimetype, encoding, etc + * will be copied across by this class. + * + * @return Returns a reader onto the location referenced by this instance. + * The instance must always be a new instance. + * @throws ContentIOException + */ + protected abstract ContentReader createReader() throws ContentIOException; + + /** + * Performs checks and copies required reader attributes + */ + public final ContentReader getReader() throws ContentIOException + { + ContentReader reader = createReader(); + if (reader == null) + { + throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" + + " reader: " + this); + } + else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(getContentUrl())) + { + throw new AlfrescoRuntimeException("ContentReader has different URL: \n" + + " reader: " + this + "\n" + + " new reader: " + reader); + } + // copy across common attributes + reader.setMimetype(this.getMimetype()); + reader.setEncoding(this.getEncoding()); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Reader spawned new reader: \n" + + " reader: " + this + "\n" + + " new reader: " + reader); + } + return reader; + } + + /** + * An automatically created listener sets the flag + */ + public synchronized final boolean isClosed() + { + if (channel != null) + { + return !channel.isOpen(); + } + else + { + return false; + } + } + + /** + * Provides low-level access to read content from the repository. + *

    + * This is the only of the content reading methods that needs to be implemented + * by derived classes. All other content access methods make use of this in their + * underlying implementations. + * + * @return Returns a channel from which content can be read + * @throws ContentIOException if the channel could not be opened or the underlying content + * has disappeared + */ + protected abstract ReadableByteChannel getDirectReadableChannel() throws ContentIOException; + + /** + * Optionally override to supply an alternate callback channel. + * + * @param directChannel the result of {@link #getDirectReadableChannel()} + * @param listeners the listeners to call + * @return Returns a channel + * @throws ContentIOException + */ + protected ReadableByteChannel getCallbackReadableChannel( + ReadableByteChannel directChannel, + List listeners) + throws ContentIOException + { + // introduce an advistor to handle the callbacks to the listeners + ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners); + ProxyFactory proxyFactory = new ProxyFactory(directChannel); + proxyFactory.addAdvice(advise); + ReadableByteChannel callbackChannel = (ReadableByteChannel) proxyFactory.getProxy(); + // done + return callbackChannel; + } + + /** + * @see #getDirectReadableChannel() + * @see #getCallbackReadableChannel() + */ + public synchronized final ReadableByteChannel getReadableChannel() throws ContentIOException + { + // this is a use-once object + if (channel != null) + { + throw new RuntimeException("A channel has already been opened"); + } + ReadableByteChannel directChannel = getDirectReadableChannel(); + channel = getCallbackReadableChannel(directChannel, listeners); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Opened channel onto content: " + this); + } + return channel; + } + + /** + * @see Channels#newInputStream(java.nio.channels.ReadableByteChannel) + */ + public InputStream getContentInputStream() throws ContentIOException + { + try + { + ReadableByteChannel channel = getReadableChannel(); + InputStream is = new BufferedInputStream(Channels.newInputStream(channel)); + // done + return is; + } + catch (Throwable e) + { + throw new ContentIOException("Failed to open stream onto channel: \n" + + " accessor: " + this, + e); + } + } + + /** + * Copies the {@link #getContentInputStream() input stream} to the given + * OutputStream + */ + public final void getContent(OutputStream os) throws ContentIOException + { + try + { + InputStream is = getContentInputStream(); + FileCopyUtils.copy(is, os); // both streams are closed + // done + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content to output stream: \n" + + " accessor: " + this, + e); + } + } + + public final void getContent(File file) throws ContentIOException + { + try + { + InputStream is = getContentInputStream(); + FileOutputStream os = new FileOutputStream(file); + FileCopyUtils.copy(is, os); // both streams are closed + // done + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content to file: \n" + + " accessor: " + this + "\n" + + " file: " + file, + e); + } + } + + public final String getContentString(int length) throws ContentIOException + { + if (length < 0 || length > Integer.MAX_VALUE) + { + throw new IllegalArgumentException("Character count must be positive and within range"); + } + Reader reader = null; + try + { + // just create buffer of the required size + char[] buffer = new char[length]; + + String encoding = getEncoding(); + // create a reader from the input stream + if (encoding == null) + { + reader = new InputStreamReader(getContentInputStream()); + } + else + { + reader = new InputStreamReader(getContentInputStream(), encoding); + } + // read it all, if possible + int count = reader.read(buffer, 0, length); + // there may have been fewer characters - create a new string + String result = new String(buffer, 0, count); + // done + return result; + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content to string: \n" + + " accessor: " + this + "\n" + + " length: " + length, + e); + } + finally + { + if (reader != null) + { + try { reader.close(); } catch (Throwable e) { logger.error(e); } + } + } + } + + /** + * Makes use of the encoding, if available, to convert bytes to a string. + *

    + * All the content is streamed into memory. So, like the interface said, + * be careful with this method. + * + * @see ContentAccessor#getEncoding() + */ + public final String getContentString() throws ContentIOException + { + try + { + // read from the stream into a byte[] + InputStream is = getContentInputStream(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + FileCopyUtils.copy(is, os); // both streams are closed + byte[] bytes = os.toByteArray(); + // get the encoding for the string + String encoding = getEncoding(); + // create the string from the byte[] using encoding if necessary + String content = (encoding == null) ? new String(bytes) : new String(bytes, encoding); + // done + return content; + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content to string: \n" + + " accessor: " + this, + e); + } + } +} diff --git a/source/java/org/alfresco/repo/content/AbstractContentStore.java b/source/java/org/alfresco/repo/content/AbstractContentStore.java new file mode 100644 index 0000000000..6c46d3a115 --- /dev/null +++ b/source/java/org/alfresco/repo/content/AbstractContentStore.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.util.GUID; + +/** + * Base class providing support for different types of content stores. + *

    + * Since content URLs have to be consistent across all stores for + * reasons of replication and backup, the most important functionality + * provided is the generation of new content URLs and the checking of + * existing URLs. + * + * @author Derek Hulley + */ +public abstract class AbstractContentStore implements ContentStore +{ + /** + * Simple implementation that uses the + * {@link ContentReader#exists() reader's exists} method as its implementation. + */ + public boolean exists(String contentUrl) throws ContentIOException + { + ContentReader reader = getReader(contentUrl); + return reader.exists(); + } + + /** + * Creates a new content URL. This must be supported by all + * stores that are compatible with Alfresco. + * + * @return Returns a new and unique content URL + */ + public static String createNewUrl() + { + Calendar calendar = new GregorianCalendar(); + int year = calendar.get(Calendar.YEAR); + int month = calendar.get(Calendar.MONTH) + 1; // 0-based + int day = calendar.get(Calendar.DAY_OF_MONTH); + // create the URL + StringBuilder sb = new StringBuilder(20); + sb.append(STORE_PROTOCOL) + .append(year).append('/') + .append(month).append('/') + .append(day).append('/') + .append(GUID.generate()).append(".bin"); + String newContentUrl = sb.toString(); + // done + return newContentUrl; + } + + /** + * This method can be used to ensure that URLs conform to the + * required format. If subclasses have to parse the URL, + * then a call to this may not be required - provided that + * the format is checked. + *

    + * The protocol part of the URL (including legacy protocols) + * is stripped out and just the relative path is returned. + * + * @param contentUrl a URL of the content to check + * @return Returns the relative part of the URL + * @throws RuntimeException if the URL is not correct + */ + public static String getRelativePart(String contentUrl) throws RuntimeException + { + int index = 0; + if (contentUrl.startsWith(STORE_PROTOCOL)) + { + index = 8; + } + else if (contentUrl.startsWith("file://")) + { + index = 7; + } + else + { + throw new AlfrescoRuntimeException( + "All content URLs must start with " + STORE_PROTOCOL + ": \n" + + " the invalid url is: " + contentUrl); + } + + // extract the relative part of the URL + String path = contentUrl.substring(index); + // more extensive checks can be added in, but it seems overkill + if (path.length() < 10) + { + throw new AlfrescoRuntimeException( + "The content URL is invalid: \n" + + " content url: " + contentUrl); + } + return path; + } +} diff --git a/source/java/org/alfresco/repo/content/AbstractContentWriter.java b/source/java/org/alfresco/repo/content/AbstractContentWriter.java new file mode 100644 index 0000000000..9831618fef --- /dev/null +++ b/source/java/org/alfresco/repo/content/AbstractContentWriter.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentAccessor; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.util.FileCopyUtils; + +/** + * Implements all the convenience methods of the interface. The only methods + * that need to be implemented, i.e. provide low-level content access are: + *

      + *
    • {@link #getDirectWritableChannel()} to write content to the repository
    • + *
    + * + * @author Derek Hulley + */ +public abstract class AbstractContentWriter extends AbstractContentAccessor implements ContentWriter +{ + private static final Log logger = LogFactory.getLog(AbstractContentWriter.class); + + private List listeners; + private WritableByteChannel channel; + private ContentReader existingContentReader; + + /** + * @param contentUrl the content URL + * @param existingContentReader a reader of a previous version of this content + */ + protected AbstractContentWriter(String contentUrl, ContentReader existingContentReader) + { + super(contentUrl); + this.existingContentReader = existingContentReader; + + listeners = new ArrayList(2); + } + + /** + * @return Returns a reader onto the previous version of this content + */ + protected ContentReader getExistingContentReader() + { + return existingContentReader; + } + + /** + * Adds the listener after checking that the output stream isn't already in + * use. + */ + public synchronized void addListener(ContentStreamListener listener) + { + if (channel != null) + { + throw new RuntimeException("Channel is already in use"); + } + listeners.add(listener); + } + + /** + * A factory method for subclasses to implement that will ensure the proper + * implementation of the {@link ContentWriter#getReader()} method. + *

    + * Only the instance need be constructed. The required mimetype, encoding, etc + * will be copied across by this class. + *

    + * + * @return Returns a reader onto the location referenced by this instance. + * The instance must always be a new instance and never null. + * @throws ContentIOException + */ + protected abstract ContentReader createReader() throws ContentIOException; + + /** + * Performs checks and copies required reader attributes + */ + public final ContentReader getReader() throws ContentIOException + { + if (!isClosed()) + { + return null; + } + ContentReader reader = createReader(); + if (reader == null) + { + throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" + + " writer: " + this); + } + else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(getContentUrl())) + { + throw new AlfrescoRuntimeException("ContentReader has different URL: \n" + + " writer: " + this + "\n" + + " new reader: " + reader); + } + // copy across common attributes + reader.setMimetype(this.getMimetype()); + reader.setEncoding(this.getEncoding()); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Writer spawned new reader: \n" + + " writer: " + this + "\n" + + " new reader: " + reader); + } + return reader; + } + + /** + * An automatically created listener sets the flag + */ + public synchronized final boolean isClosed() + { + if (channel != null) + { + return !channel.isOpen(); + } + else + { + return false; + } + } + + /** + * Provides low-level access to write content to the repository. + *

    + * This is the only of the content writing methods that needs to be implemented + * by derived classes. All other content access methods make use of this in their + * underlying implementations. + * + * @return Returns a channel with which to write content + * @throws ContentIOException if the channel could not be opened + */ + protected abstract WritableByteChannel getDirectWritableChannel() throws ContentIOException; + + /** + * Optionally override to supply an alternate callback channel. + * + * @param directChannel the result of {@link #getDirectWritableChannel()} + * @param listeners the listeners to call + * @return Returns a callback channel + * @throws ContentIOException + */ + protected WritableByteChannel getCallbackWritableChannel( + WritableByteChannel directChannel, + List listeners) + throws ContentIOException + { + // proxy to add an advise + ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners); + ProxyFactory proxyFactory = new ProxyFactory(directChannel); + proxyFactory.addAdvice(advise); + WritableByteChannel callbackChannel = (WritableByteChannel) proxyFactory.getProxy(); + // done + return callbackChannel; + } + + /** + * @see #getDirectWritableChannel() + * @see #getCallbackWritableChannel() + */ + public synchronized final WritableByteChannel getWritableChannel() throws ContentIOException + { + // this is a use-once object + if (channel != null) + { + throw new RuntimeException("A channel has already been opened"); + } + WritableByteChannel directChannel = getDirectWritableChannel(); + channel = getCallbackWritableChannel(directChannel, listeners); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Opened channel onto content: \n" + + " content: " + this + "\n" + + " channel: " + channel); + } + return channel; + } + + /** + * @see Channels#newOutputStream(java.nio.channels.WritableByteChannel) + */ + public OutputStream getContentOutputStream() throws ContentIOException + { + try + { + WritableByteChannel channel = getWritableChannel(); + OutputStream is = new BufferedOutputStream(Channels.newOutputStream(channel)); + // done + return is; + } + catch (Throwable e) + { + throw new ContentIOException("Failed to open stream onto channel: \n" + + " writer: " + this, + e); + } + } + + /** + * @see ContentReader#getContentInputStream() + * @see #putContent(InputStream) + */ + public void putContent(ContentReader reader) throws ContentIOException + { + try + { + // get the stream to read from + InputStream is = reader.getContentInputStream(); + // put the content + putContent(is); + } + catch (Throwable e) + { + throw new ContentIOException("Failed to copy reader content to writer: \n" + + " writer: " + this + "\n" + + " source reader: " + reader, + e); + } + } + + public final void putContent(InputStream is) throws ContentIOException + { + try + { + OutputStream os = getContentOutputStream(); + FileCopyUtils.copy(is, os); // both streams are closed + // done + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content from input stream: \n" + + " writer: " + this, + e); + } + } + + public final void putContent(File file) throws ContentIOException + { + try + { + OutputStream os = getContentOutputStream(); + FileInputStream is = new FileInputStream(file); + FileCopyUtils.copy(is, os); // both streams are closed + // done + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content from file: \n" + + " writer: " + this + "\n" + + " file: " + file, + e); + } + } + + /** + * Makes use of the encoding, if available, to convert the string to bytes. + * + * @see ContentAccessor#getEncoding() + */ + public final void putContent(String content) throws ContentIOException + { + try + { + // attempt to use the correct encoding + String encoding = getEncoding(); + byte[] bytes = (encoding == null) ? content.getBytes() : content.getBytes(encoding); + // get the stream + OutputStream os = getContentOutputStream(); + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + FileCopyUtils.copy(is, os); // both streams are closed + // done + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content from string: \n" + + " writer: " + this + + " content length: " + content.length(), + e); + } + } +} diff --git a/source/java/org/alfresco/repo/content/ContentServicePolicies.java b/source/java/org/alfresco/repo/content/ContentServicePolicies.java new file mode 100644 index 0000000000..871b836af0 --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentServicePolicies.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import org.alfresco.repo.policy.ClassPolicy; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Content service policies interface + * + * @author Roy Wetherall + */ +public interface ContentServicePolicies +{ + /** + * On content update policy interface + */ + public interface OnContentUpdatePolicy extends ClassPolicy + { + /** + * @param nodeRef the node reference + */ + public void onContentUpdate(NodeRef nodeRef); + } +} diff --git a/source/java/org/alfresco/repo/content/ContentStore.java b/source/java/org/alfresco/repo/content/ContentStore.java new file mode 100644 index 0000000000..e6e1fba7bd --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentStore.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.util.Set; + +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.cmr.repository.ContentWriter; + +/** + * Provides low-level retrieval of content + * {@link org.alfresco.service.cmr.repository.ContentReader readers} and + * {@link org.alfresco.service.cmr.repository.ContentWriter writers}. + *

    + * Implementations of this interface should be soley responsible for + * providing persistence and retrieval of the content against a + * content URL. + *

    + * The URL format is store://year/month/day/GUID.bin
    + *

      + *
    • store://: prefix identifying an Alfresco content stores + * regardless of the persistence mechanism.
    • + *
    • year: year
    • + *
    • month: 1-based month of the year
    • + *
    • day: 1-based day of the month
    • + *
    • GUID: A unique identifier
    • + *
    + * The old file:// prefix must still be supported - and functionality + * around this can be found in the {@link org.alfresco.repo.content.AbstractContentStore} + * implementation. + * + * @author Derek Hulley + */ +public interface ContentStore +{ + /** store:// is the new prefix for all content URLs */ + public static final String STORE_PROTOCOL = "store://"; + + /** + * Check for the existence of content in the store. + *

    + * The implementation of this may be more efficient than first getting a + * reader to {@link ContentReader#exists() check for existence}, although + * that check should also be performed. + * + * @param contentUrl the path to the content + * @return Returns true if the content exists. + * @throws ContentIOException + * + * @see ContentReader#exists() + */ + public boolean exists(String contentUrl) throws ContentIOException; + + /** + * Get the accessor with which to read from the content + * at the given URL. The reader is stateful and + * can only be used once. + * + * @param contentUrl the path to where the content is located + * @return Returns a read-only content accessor for the given URL. There may + * be no content at the given URL, but the reader must still be returned. + * The reader may implement the {@link RandomAccessContent random access interface}. + * @throws ContentIOException + * + * @see #exists(String) + * @see ContentReader#exists() + */ + public ContentReader getReader(String contentUrl) throws ContentIOException; + + /** + * Get an accessor with which to write content to a location + * within the store. The writer is stateful and can + * only be used once. The location may be specified but must, in that case, + * be a valid and unused URL. + *

    + * By supplying a reader to existing content, the store implementation may + * enable {@link RandomAccessContent random access}. The store implementation + * can enable this by copying the existing content into the new location + * before supplying a writer onto the new content. + * + * @param existingContentReader a reader onto any existing content for which + * a writer is required - may be null + * @param newContentUrl an unused, valid URL to use - may be null. + * @return Returns a write-only content accessor, possibly implementing + * the {@link RandomAccessContent random access interface} + * @throws ContentIOException if completely new content storage could not be + * created + * + * @see ContentWriter#addListener(ContentStreamListener) + * @see ContentWriter#getContentUrl() + */ + public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException; + + /** + * Get a set of all content in the store + * + * @return Returns a complete set of the unique URLs of all available content + * in the store + * @throws ContentIOException + */ + public Set getUrls() throws ContentIOException; + + /** + * Deletes the content at the given URL. + *

    + * A delete cannot be forced since it is much better to have the + * file remain longer than desired rather than deleted prematurely. + * The store implementation should safeguard files for certain + * minimum period, in which case all files younger than a certain + * age will not be deleted. + * + * @param contentUrl the URL of the content to delete + * @return Return true if the content was deleted (either by this or + * another operation), otherwise false + * @throws ContentIOException + */ + public boolean delete(String contentUrl) throws ContentIOException; +} diff --git a/source/java/org/alfresco/repo/content/ContentStoreCleanupJob.java b/source/java/org/alfresco/repo/content/ContentStoreCleanupJob.java new file mode 100644 index 0000000000..550d380b5c --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentStoreCleanupJob.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.search.SearchService; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Removes all content form the store that is not referenced by any content node. + *

    + * The following parameters are required: + *

      + *
    • contentStore: The content store bean to clean up
    • + *
    • searcher: The index searcher that searches for content in the store
    • + *
    • protectHours: The number of hours to protect content that isn't referenced
    • + *
    + * + * @author Derek Hulley + */ +public class ContentStoreCleanupJob implements Job +{ + /** + * Gets all content URLs from the store, checks if it is in use by any node + * and deletes those that aren't. + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap jobData = context.getJobDetail().getJobDataMap(); + // extract the content store to use + Object contentStoreObj = jobData.get("contentStore"); + if (contentStoreObj == null || !(contentStoreObj instanceof ContentStore)) + { + throw new AlfrescoRuntimeException( + "ContentStoreCleanupJob data must contain valid 'contentStore' reference"); + } + ContentStore contentStore = (ContentStore) contentStoreObj; + // extract the search to use + Object searcherObj = jobData.get("searcher"); + if (searcherObj == null || !(searcherObj instanceof SearchService)) + { + throw new AlfrescoRuntimeException( + "ContentStoreCleanupJob data must contain valid 'searcher' reference"); + } + SearchService searcher = (SearchService) searcherObj; + // get the number of hourse to protect content + Object protectHoursObj = jobData.get("protectHours"); + if (protectHoursObj == null || !(protectHoursObj instanceof String)) + { + throw new AlfrescoRuntimeException( + "ContentStoreCleanupJob data must contain valid 'protectHours' value"); + } + long protectHours = 24L; + try + { + protectHours = Long.parseLong((String) protectHoursObj); + } + catch (NumberFormatException e) + { + throw new AlfrescoRuntimeException( + "ContentStoreCleanupJob data 'protectHours' value is not a valid integer"); + } + + long protectMillis = protectHours * 3600L * 1000L; // 3600s in an hour; 1000ms in a second + long now = System.currentTimeMillis(); + long lastModifiedSafeTimeMs = (now - protectMillis); // able to remove anything modified before this + + // get all URLs in the store + Set contentUrls = contentStore.getUrls(); + for (String contentUrl : contentUrls) + { + // TODO here we need to get hold of all the orphaned content in this store + + // not found - it is not in the repo, but check that it is old enough to delete + ContentReader reader = contentStore.getReader(contentUrl); + if (reader == null || !reader.exists()) + { + // gone missing in the meantime + continue; + } + long lastModified = reader.getLastModified(); + if (lastModified >= lastModifiedSafeTimeMs) + { + // not old enough + continue; + } + + // it is not in the repo and is old enough + boolean result = contentStore.delete(contentUrl); + System.out.println(contentUrl + ": " + Boolean.toString(result)); + } + + // TODO for now throw this exception to ensure that this job does not get run until + // the orphaned content can be correctly retrieved + throw new UnsupportedOperationException(); + } +} diff --git a/source/java/org/alfresco/repo/content/ContentStoreCleanupJobTest.java b/source/java/org/alfresco/repo/content/ContentStoreCleanupJobTest.java new file mode 100644 index 0000000000..c975cf68e8 --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentStoreCleanupJobTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.util.Date; + +import junit.framework.TestSuite; + +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.BaseSpringTest; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.Scheduler; +import org.quartz.SchedulerFactory; +import org.quartz.impl.StdSchedulerFactory; +import org.quartz.impl.calendar.BaseCalendar; +import org.quartz.spi.TriggerFiredBundle; +import org.springframework.scheduling.quartz.SimpleTriggerBean; + +/** + * Content store cleanup job unit test + * + * @author Roy Wetherall + */ +public class ContentStoreCleanupJobTest extends BaseSpringTest +{ + private SimpleTriggerBean simpleTriggerBean; + private JobExecutionContext jobExecutionContext; + private ContentStoreCleanupJob job; + + private ContentStore contentStore; + private String url; + + /** + * This can be removed once the class being tested actually has a remote + * chance of working. + */ + public static TestSuite suite() + { + return new TestSuite(); + } + + /** + * On setup in transaction + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + this.contentStore = (ContentStore)this.applicationContext.getBean("fileContentStore"); + this.simpleTriggerBean = (SimpleTriggerBean)this.applicationContext.getBean("fileContentStoreCleanerTrigger"); + + SchedulerFactory factory = new StdSchedulerFactory(); + Scheduler scheduler = factory.getScheduler(); + + // Set the protect hours to 0 for the purpose of this test + JobDataMap jobDataMap = this.simpleTriggerBean.getJobDetail().getJobDataMap(); + jobDataMap.put("protectHours", "0"); + this.simpleTriggerBean.getJobDetail().setJobDataMap(jobDataMap); + + this.job = new ContentStoreCleanupJob(); + TriggerFiredBundle triggerFiredBundle = new TriggerFiredBundle( + this.simpleTriggerBean.getJobDetail(), + this.simpleTriggerBean, + new BaseCalendar(), + false, + new Date(), + new Date(), + new Date(), + new Date()); + + this.jobExecutionContext = new JobExecutionContext(scheduler, triggerFiredBundle, job); + + ContentWriter contentWriter = this.contentStore.getWriter(null, null); + contentWriter.putContent("This is some content that I am going to delete."); + this.url = contentWriter.getContentUrl(); + } + + /** + * Test execute method + */ + public void testExecute() + { + try + { + ContentReader before = this.contentStore.getReader(this.url); + assertTrue(before.exists()); + + this.job.execute(this.jobExecutionContext); + + ContentReader after = this.contentStore.getReader(this.url); + assertFalse(after.exists()); + } + catch (JobExecutionException exception) + { + fail("Exception raised!"); + } + } +} diff --git a/source/java/org/alfresco/repo/content/MimetypeMap.java b/source/java/org/alfresco/repo/content/MimetypeMap.java new file mode 100644 index 0000000000..9f75b5e193 --- /dev/null +++ b/source/java/org/alfresco/repo/content/MimetypeMap.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.config.Config; +import org.alfresco.config.ConfigElement; +import org.alfresco.config.ConfigLookupContext; +import org.alfresco.config.ConfigService; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides a bidirectional mapping between well-known mimetypes and + * the registered file extensions. All mimetypes and extensions + * are stored and handled as lowercase. + * + * @author Derek Hulley + */ +public class MimetypeMap implements MimetypeService +{ + public static final String MIMETYPE_TEXT_PLAIN = "text/plain"; + public static final String MIMETYPE_TEXT_CSS = "text/css"; + public static final String MIMETYPE_XML = "text/xml"; + public static final String MIMETYPE_HTML = "text/html"; + public static final String MIMETYPE_PDF = "application/pdf"; + public static final String MIMETYPE_WORD = "application/msword"; + public static final String MIMETYPE_EXCEL = "application/vnd.excel"; + public static final String MIMETYPE_BINARY = "application/octet-stream"; + public static final String MIMETYPE_PPT = "application/vnd.powerpoint"; + public static final String MIMETYPE_FLASH = "application/x-shockwave-flash"; + public static final String MIMETYPE_IMAGE_GIF = "image/gif"; + public static final String MIMETYPE_IMAGE_JPEG = "image/jpeg"; + public static final String MIMETYPE_IMAGE_RGB = "image/x-rgb"; + public static final String MIMETYPE_OPENDOCUMENT_TEXT = "application/vnd.oasis.opendocument.text"; + public static final String MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE = "application/vnd.oasis.opendocument.text-template"; + public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS = "application/vnd.oasis.opendocument.graphics"; + public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS_TEMPLATE= "application/vnd.oasis.opendocument.graphics-template"; + public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION= "application/vnd.oasis.opendocument.presentation"; + public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE= "application/vnd.oasis.opendocument.presentation-template"; + public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET= "application/vnd.oasis.opendocument.spreadsheet"; + public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE= "application/vnd.oasis.opendocument.spreadsheet-template"; + public static final String MIMETYPE_OPENDOCUMENT_CHART= "application/vnd.oasis.opendocument.chart"; + public static final String MIMETYPE_OPENDOCUMENT_CHART_TEMPLATE= "applicationvnd.oasis.opendocument.chart-template"; + public static final String MIMETYPE_OPENDOCUMENT_IMAGE= "application/vnd.oasis.opendocument.image"; + public static final String MIMETYPE_OPENDOCUMENT_IMAGE_TEMPLATE= "applicationvnd.oasis.opendocument.image-template"; + public static final String MIMETYPE_OPENDOCUMENT_FORMULA= "application/vnd.oasis.opendocument.formula"; + public static final String MIMETYPE_OPENDOCUMENT_FORMULA_TEMPLATE= "applicationvnd.oasis.opendocument.formula-template"; + public static final String MIMETYPE_OPENDOCUMENT_TEXT_MASTER= "application/vnd.oasis.opendocument.text-master"; + public static final String MIMETYPE_OPENDOCUMENT_TEXT_WEB= "application/vnd.oasis.opendocument.text-web"; + public static final String MIMETYPE_OPENDOCUMENT_DATABASE= "application/vnd.oasis.opendocument.database"; + public static final String MIMETYPE_OPENOFFICE_WRITER = "application/vnd.sun.xml.writer"; + public static final String MIMETYPE_MP3 = "audio/x-mpeg"; + public static final String MIMETYPE_ACP = "application/acp"; + + private static final String CONFIG_AREA = "mimetype-map"; + private static final String CONFIG_CONDITION = "Mimetype Map"; + private static final String ELEMENT_MIMETYPES = "mimetypes"; + private static final String ATTR_MIMETYPE = "mimetype"; + private static final String ATTR_DISPLAY = "display"; + private static final String ATTR_DEFAULT = "default"; + + private static final Log logger = LogFactory.getLog(MimetypeMap.class); + + private ConfigService configService; + + private List mimetypes; + private Map extensionsByMimetype; + private Map mimetypesByExtension; + private Map displaysByMimetype; + private Map displaysByExtension; + + /** + * @param configService the config service to use to read mimetypes from + */ + public MimetypeMap(ConfigService configService) + { + this.configService = configService; + } + + /** + * Initialises the map using the configuration service provided + */ + public void init() + { + this.mimetypes = new ArrayList(40); + this.extensionsByMimetype = new HashMap(59); + this.mimetypesByExtension = new HashMap(59); + this.displaysByMimetype = new HashMap(59); + this.displaysByExtension = new HashMap(59); + + Config config = configService.getConfig(CONFIG_CONDITION, new ConfigLookupContext(CONFIG_AREA)); + ConfigElement mimetypesElement = config.getConfigElement(ELEMENT_MIMETYPES); + List mimetypes = mimetypesElement.getChildren(); + int count = 0; + for (ConfigElement mimetypeElement : mimetypes) + { + count++; + // add to list of mimetypes + String mimetype = mimetypeElement.getAttribute(ATTR_MIMETYPE); + if (mimetype == null || mimetype.length() == 0) + { + logger.warn("Ignoring empty mimetype " + count); + continue; + } + // we store it as lowercase + mimetype = mimetype.toLowerCase(); + if (this.mimetypes.contains(mimetype)) + { + throw new AlfrescoRuntimeException("Duplicate mimetype definition: " + mimetype); + } + this.mimetypes.add(mimetype); + // add to map of mimetype displays + String mimetypeDisplay = mimetypeElement.getAttribute(ATTR_DISPLAY); + if (mimetypeDisplay != null && mimetypeDisplay.length() > 0) + { + this.displaysByMimetype.put(mimetype, mimetypeDisplay); + } + + // get all the extensions + boolean isFirst = true; + List extensions = mimetypeElement.getChildren(); + for (ConfigElement extensionElement : extensions) + { + // add to map of mimetypes by extension + String extension = extensionElement.getValue(); + if (extension == null || extension.length() == 0) + { + logger.warn("Ignoring empty extension for mimetype: " + mimetype); + continue; + } + // put to lowercase + extension = extension.toLowerCase(); + this.mimetypesByExtension.put(extension, mimetype); + // add to map of extension displays + String extensionDisplay = extensionElement.getAttribute(ATTR_DISPLAY); + if (extensionDisplay != null && extensionDisplay.length() > 0) + { + this.displaysByExtension.put(extension, extensionDisplay); + } + else if (mimetypeDisplay != null && mimetypeDisplay.length() > 0) + { + // no display defined for the extension - use the mimetype's display + this.displaysByExtension.put(extension, mimetypeDisplay); + } + // add to map of extensions by mimetype if it is the default or first extension + String isDefaultStr = extensionElement.getAttribute(ATTR_DEFAULT); + boolean isDefault = Boolean.parseBoolean(isDefaultStr); + if (isDefault || isFirst) + { + this.extensionsByMimetype.put(mimetype, extension); + } + isFirst = false; + } + // check that there were extensions defined + if (extensions.size() == 0) + { + logger.warn("No extensions defined for mimetype: " + mimetype); + } + } + + // make the collections read-only + this.mimetypes = Collections.unmodifiableList(this.mimetypes); + this.extensionsByMimetype = Collections.unmodifiableMap(this.extensionsByMimetype); + this.mimetypesByExtension = Collections.unmodifiableMap(this.mimetypesByExtension); + this.displaysByMimetype = Collections.unmodifiableMap(this.displaysByMimetype); + this.displaysByExtension = Collections.unmodifiableMap(this.displaysByExtension); + } + + /** + * @param mimetype a valid mimetype + * @return Returns the default extension for the mimetype + * @throws AlfrescoRuntimeException if the mimetype doesn't exist + */ + public String getExtension(String mimetype) + { + String extension = extensionsByMimetype.get(mimetype); + if (extension == null) + { + throw new AlfrescoRuntimeException("No extension available for mimetype: " + mimetype); + } + return extension; + } + + public Map getDisplaysByExtension() + { + return displaysByExtension; + } + + public Map getDisplaysByMimetype() + { + return displaysByMimetype; + } + + public Map getExtensionsByMimetype() + { + return extensionsByMimetype; + } + + public List getMimetypes() + { + return mimetypes; + } + + public Map getMimetypesByExtension() + { + return mimetypesByExtension; + } + + /** + * @see #MIMETYPE_BINARY + */ + public String guessMimetype(String filename) + { + filename = filename.toLowerCase(); + String mimetype = MIMETYPE_BINARY; + // extract the extension + int index = filename.lastIndexOf('.'); + if (index > -1 && (index < filename.length() - 1)) + { + String extension = filename.substring(index + 1); + if (mimetypesByExtension.containsKey(extension)) + { + mimetype = mimetypesByExtension.get(extension); + } + } + return mimetype; + } +} diff --git a/source/java/org/alfresco/repo/content/MimetypeMapTest.java b/source/java/org/alfresco/repo/content/MimetypeMapTest.java new file mode 100644 index 0000000000..ec722faf47 --- /dev/null +++ b/source/java/org/alfresco/repo/content/MimetypeMapTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.util.Map; + +import org.alfresco.util.BaseSpringTest; + +/** + * @see org.alfresco.repo.content.MimetypeMap + * + * @author Derek Hulley + */ +public class MimetypeMapTest extends BaseSpringTest +{ + private MimetypeMap mimetypeMap; + + public void setMimetypeMap(MimetypeMap mimetypeMap) + { + this.mimetypeMap = mimetypeMap; + } + + public void testExtensions() throws Exception + { + Map extensionsByMimetype = mimetypeMap.getExtensionsByMimetype(); + Map mimetypesByExtension = mimetypeMap.getMimetypesByExtension(); + + // plain text + assertEquals("txt", extensionsByMimetype.get("text/plain")); + assertEquals("text/plain", mimetypesByExtension.get("txt")); + assertEquals("text/plain", mimetypesByExtension.get("csv")); + assertEquals("text/plain", mimetypesByExtension.get("java")); + + // JPEG + assertEquals("jpg", extensionsByMimetype.get("image/jpeg")); + assertEquals("image/jpeg", mimetypesByExtension.get("jpg")); + assertEquals("image/jpeg", mimetypesByExtension.get("jpeg")); + assertEquals("image/jpeg", mimetypesByExtension.get("jpe")); + + // MS Word + assertEquals("doc", extensionsByMimetype.get("application/msword")); + assertEquals("application/msword", mimetypesByExtension.get("doc")); + } +} diff --git a/source/java/org/alfresco/repo/content/RandomAccessContent.java b/source/java/org/alfresco/repo/content/RandomAccessContent.java new file mode 100644 index 0000000000..9a1f39a5de --- /dev/null +++ b/source/java/org/alfresco/repo/content/RandomAccessContent.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.nio.channels.FileChannel; + +import org.alfresco.service.cmr.repository.ContentIOException; + +/** + * Supplementary interface for content readers and writers that allow random-access to + * the underlying content. + *

    + * The use of this interface by a client may preclude the use of any other + * access to the underlying content - this depends on the underlying implementation. + * + * @author Derek Hulley + */ +public interface RandomAccessContent +{ + /** + * @return Returns true if the content can be written to + */ + public boolean canWrite(); + + /** + * Get a channel to access the content. The channel's behaviour is similar to that + * when a FileChannel is retrieved using {@link java.io.RandomAccessFile#getChannel()}. + * + * @return Returns a channel to access the content + * @throws ContentIOException + * + * @see #canWrite() + * @see java.io.RandomAccessFile#getChannel() + */ + public FileChannel getChannel() throws ContentIOException; +} diff --git a/source/java/org/alfresco/repo/content/RoutingContentService.java b/source/java/org/alfresco/repo/content/RoutingContentService.java new file mode 100644 index 0000000000..7278659fbf --- /dev/null +++ b/source/java/org/alfresco/repo/content/RoutingContentService.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; +import org.alfresco.repo.content.filestore.FileContentStore; +import org.alfresco.repo.content.transform.ContentTransformer; +import org.alfresco.repo.content.transform.ContentTransformerRegistry; +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NoTransformerException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.EqualsHelper; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * A content service that determines at runtime the store that the + * content associated with a node should be routed to. + * + * @author Derek Hulley + */ +public class RoutingContentService implements ContentService +{ + private static Log logger = LogFactory.getLog(RoutingContentService.class); + + private TransactionService transactionService; + private DictionaryService dictionaryService; + private NodeService nodeService; + /** a registry of all available content transformers */ + private ContentTransformerRegistry transformerRegistry; + /** TEMPORARY until we have a map to choose from at runtime */ + private ContentStore store; + /** the store for all temporarily created content */ + private ContentStore tempStore; + + /** + * The policy component + */ + private PolicyComponent policyComponent; + + /** + * The onContentService policy delegate + */ + ClassPolicyDelegate onContentUpdateDelegate; + + /** + * Default constructor sets up a temporary store + */ + public RoutingContentService() + { + this.tempStore = new FileContentStore(TempFileProvider.getTempDir().getAbsolutePath()); + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setTransformerRegistry(ContentTransformerRegistry transformerRegistry) + { + this.transformerRegistry = transformerRegistry; + } + + public void setStore(ContentStore store) + { + this.store = store; + } + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Service initialise + */ + public void init() + { + // Bind on update properties behaviour + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + this, + new JavaBehaviour(this, "onUpdateProperties")); + + // Register on content update policy + this.onContentUpdateDelegate = this.policyComponent.registerClassPolicy(OnContentUpdatePolicy.class); + } + + /** + * Update properties policy behaviour + * + * @param nodeRef the node reference + * @param before the before values of the properties + * @param after the after values of the properties + */ + public void onUpdateProperties( + NodeRef nodeRef, + Map before, + Map after) + { + boolean fire = false; + // check if any of the content properties have changed + for (QName propertyQName : after.keySet()) + { + // is this a content property? + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + if (propertyDef == null) + { + // the property is not recognised + continue; + } + if (!propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + // not a content type + continue; + } + + try + { + ContentData beforeValue = (ContentData) before.get(propertyQName); + ContentData afterValue = (ContentData) after.get(propertyQName); + if (afterValue != null && afterValue.getContentUrl() == null) + { + // no URL - ignore + } + else if (!EqualsHelper.nullSafeEquals(beforeValue, afterValue)) + { + // the content changed + // at the moment, we are only interested in this one change + fire = true; + break; + } + } + catch (ClassCastException e) + { + // properties don't conform to model + continue; + } + } + // fire? + if (fire) + { + // Fire the content update policy + Set types = new HashSet(this.nodeService.getAspects(nodeRef)); + types.add(this.nodeService.getType(nodeRef)); + OnContentUpdatePolicy policy = this.onContentUpdateDelegate.get(types); + policy.onContentUpdate(nodeRef); + } + } + + public ContentReader getReader(NodeRef nodeRef, QName propertyQName) + { + // ensure that the node property is of type content + PropertyDefinition contentPropDef = dictionaryService.getProperty(propertyQName); + if (contentPropDef == null || !contentPropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + throw new InvalidTypeException("The node property must be of type content: \n" + + " node: " + nodeRef + "\n" + + " property name: " + propertyQName + "\n" + + " property type: " + ((contentPropDef == null) ? "unknown" : contentPropDef.getDataType()), + propertyQName); + } + + // get the content property + ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, propertyQName); + // check that the URL is available + if (contentData == null || contentData.getContentUrl() == null) + { + // there is no URL - the interface specifies that this is not an error condition + return null; + } + String contentUrl = contentData.getContentUrl(); + + // TODO: Choose the store to read from at runtime + ContentReader reader = store.getReader(contentUrl); + + // set extra data on the reader + reader.setMimetype(contentData.getMimetype()); + reader.setEncoding(contentData.getEncoding()); + + // we don't listen for anything + // result may be null - but interface contract says we may return null + return reader; + } + + public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) + { + // check for an existing URL - the get of the reader will perform type checking + ContentReader existingContentReader = getReader(nodeRef, propertyQName); + + // TODO: Choose the store to write to at runtime + + // get the content using the (potentially) existing content - the new content + // can be wherever the store decides. + ContentWriter writer = store.getWriter(existingContentReader, null); + + // set extra data on the reader if the property is pre-existing + ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, propertyQName); + if (contentData != null) + { + writer.setMimetype(contentData.getMimetype()); + writer.setEncoding(contentData.getEncoding()); + } + + // attach a listener if required + if (update) + { + // need a listener to update the node when the stream closes + WriteStreamListener listener = new WriteStreamListener(nodeService, nodeRef, propertyQName, writer); + writer.addListener(listener); + writer.setTransactionService(transactionService); + } + + // give back to the client + return writer; + } + + /** + * @return Returns a writer to an anonymous location + */ + public ContentWriter getTempWriter() + { + // there is no existing content and we don't specify the location of the new content + return tempStore.getWriter(null, null); + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + */ + public void transform(ContentReader reader, ContentWriter writer) + throws NoTransformerException, ContentIOException + { + // check that source and target mimetypes are available + String sourceMimetype = reader.getMimetype(); + if (sourceMimetype == null) + { + throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader); + } + String targetMimetype = writer.getMimetype(); + if (targetMimetype == null) + { + throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer); + } + // look for a transformer + ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype); + if (transformer == null) + { + throw new NoTransformerException(sourceMimetype, targetMimetype); + } + // we have a transformer, so do it + transformer.transform(reader, writer); + // done + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + */ + public boolean isTransformable(ContentReader reader, ContentWriter writer) + { + // check that source and target mimetypes are available + String sourceMimetype = reader.getMimetype(); + if (sourceMimetype == null) + { + throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader); + } + String targetMimetype = writer.getMimetype(); + if (targetMimetype == null) + { + throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer); + } + + // look for a transformer + ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype); + return (transformer != null); + } + + /** + * Ensures that, upon closure of the output stream, the node is updated with + * the latest URL of the content to which it refers. + *

    + * The listener close operation does not need a transaction as the + * ContentWriter takes care of that. + * + * @author Derek Hulley + */ + private static class WriteStreamListener implements ContentStreamListener + { + private NodeService nodeService; + private NodeRef nodeRef; + private QName propertyQName; + private ContentWriter writer; + + public WriteStreamListener( + NodeService nodeService, + NodeRef nodeRef, + QName propertyQName, + ContentWriter writer) + { + this.nodeService = nodeService; + this.nodeRef = nodeRef; + this.propertyQName = propertyQName; + this.writer = writer; + } + + public void contentStreamClosed() throws ContentIOException + { + try + { + // set the full content property + ContentData contentData = writer.getContentData(); + nodeService.setProperty( + nodeRef, + propertyQName, + contentData); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Stream listener updated node: \n" + + " node: " + nodeRef + "\n" + + " property: " + propertyQName + "\n" + + " value: " + contentData); + } + } + catch (Throwable e) + { + throw new ContentIOException("Failed to set content property on stream closure: \n" + + " node: " + nodeRef + "\n" + + " property: " + propertyQName + "\n" + + " writer: " + writer, + e); + } + } + } +} diff --git a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java new file mode 100644 index 0000000000..1e1733e904 --- /dev/null +++ b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import javax.transaction.RollbackException; +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NoTransformerException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyMap; +import org.alfresco.util.TempFileProvider; + +/** + * @see org.alfresco.repo.content.RoutingContentService + * + * @author Derek Hulley + */ +public class RoutingContentServiceTest extends BaseSpringTest +{ + private static final String SOME_CONTENT = "ABC"; + + private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/RoutingContentServiceTest"; + + private ContentService contentService; + private PolicyComponent policyComponent; + private NodeService nodeService; + private NodeRef rootNodeRef; + private NodeRef contentNodeRef; + private AuthenticationComponent authenticationComponent; + + public RoutingContentServiceTest() + { + } + + @Override + public void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + nodeService = (NodeService) applicationContext.getBean("dbNodeService"); + contentService = (ContentService) applicationContext.getBean(ServiceRegistry.CONTENT_SERVICE.getLocalName()); + this.policyComponent = (PolicyComponent)this.applicationContext.getBean("policyComponent"); + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + // create a store and get the root node + StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, getName()); + if (!nodeService.exists(storeRef)) + { + storeRef = nodeService.createStore(storeRef.getProtocol(), storeRef.getIdentifier()); + } + rootNodeRef = nodeService.getRootNode(storeRef); + // create a content node + ContentData contentData = new ContentData(null, "text/plain", 0L, "UTF-16"); + + PropertyMap properties = new PropertyMap(); + properties.put(ContentModel.PROP_CONTENT, contentData); + + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, GUID.generate()), + ContentModel.TYPE_CONTENT, + properties); + contentNodeRef = assocRef.getChildRef(); + } + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + private UserTransaction getUserTransaction() + { + TransactionService transactionService = (TransactionService)applicationContext.getBean("transactionComponent"); + return (UserTransaction) transactionService.getUserTransaction(); + } + + public void testSetUp() throws Exception + { + assertNotNull(contentService); + assertNotNull(nodeService); + assertNotNull(rootNodeRef); + assertNotNull(contentNodeRef); + assertNotNull(getUserTransaction()); + assertFalse(getUserTransaction() == getUserTransaction()); // ensure txn instances aren't shared + } + + /** + * Checks that the URL, mimetype and encoding are automatically set on the readers + * and writers + */ + public void testAutoSettingOfProperties() throws Exception + { + // get a writer onto the node + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + assertNotNull("Writer should not be null", writer); + assertNotNull("Content URL should not be null", writer.getContentUrl()); + assertNotNull("Content mimetype should not be null", writer.getMimetype()); + assertNotNull("Content encoding should not be null", writer.getEncoding()); + + // write some content + writer.putContent(SOME_CONTENT); + + // get the reader + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNotNull("Reader should not be null", reader); + assertNotNull("Content URL should not be null", reader.getContentUrl()); + assertNotNull("Content mimetype should not be null", reader.getMimetype()); + assertNotNull("Content encoding should not be null", reader.getEncoding()); + + // check that the content length is correct + // - note encoding is important as we get the byte length + long length = SOME_CONTENT.getBytes(reader.getEncoding()).length; // ensures correct decoding + long checkLength = reader.getSize(); + assertEquals("Content length incorrect", length, checkLength); + + // check the content - the encoding will come into effect here + String contentCheck = reader.getContentString(); + assertEquals("Content incorrect", SOME_CONTENT, contentCheck); + } + + public void testWriteToNodeWithoutAnyContentProperties() throws Exception + { + // previously, the node was populated with the mimetype, etc + // check that the write has these + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + assertNotNull(writer.getMimetype()); + assertNotNull(writer.getEncoding()); + + // now remove the content property from the node + nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, null); + + writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + assertNull(writer.getMimetype()); + assertEquals("UTF-8", writer.getEncoding()); + + // now set it on the writer + writer.setMimetype("text/plain"); + writer.setEncoding("UTF-8"); + + String content = "The quick brown fox ..."; + writer.putContent(content); + + // the properties should have found their way onto the node + ContentData contentData = (ContentData) nodeService.getProperty(contentNodeRef, ContentModel.PROP_CONTENT); + assertEquals("metadata didn't get onto node", writer.getContentData(), contentData); + + // check that the reader's metadata is set + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertEquals("Metadata didn't get set on reader", writer.getContentData(), reader.getContentData()); + } + + public void testNullReaderForNullUrl() throws Exception + { + // set the property, but with a null URL + ContentData contentData = new ContentData(null, null, 0L, null); + nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, null); + + // get the reader + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNull("Reader must be null if the content URL is null", reader); + } + + /** + * Checks what happens when the physical content disappears + */ + public void testMissingContent() throws Exception + { + File tempFile = TempFileProvider.createTempFile(getName(), ".txt"); + + ContentWriter writer = new FileContentWriter(tempFile); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("What about the others? Buckwheats!"); + // check + assertTrue("File does not exist", tempFile.exists()); + assertTrue("File not written to", tempFile.length() > 0); + + // update the node with this new info + ContentData contentData = writer.getContentData(); + nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, contentData); + + // delete the content + tempFile.delete(); + assertFalse("File not deleted", tempFile.exists()); + + // check the indexing doesn't spank everthing + setComplete(); + endTransaction(); + + // now attempt to get the reader for the node + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + } + + /** + * Tests simple writes that don't automatically update the node content URL + */ + public void testSimpleWrite() throws Exception + { + // get a writer to an arbitrary node + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false); // no updating of URL + assertNotNull("Writer should not be null", writer); + + // put some content + writer.putContent(SOME_CONTENT); + + // get the reader for the node + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNull("No reader should yet be available for the node", reader); + } + + private boolean policyFired = false; + + /** + * Tests that the content update policy firs correctly + */ + public void testOnContentUpdatePolicy() + { + // Register interest in the content update event for a versionable node + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onContentUpdate"), + ContentModel.ASPECT_VERSIONABLE, + new JavaBehaviour(this, "onContentUpdateBehaviourTest")); + + // First check that the policy is not fired when the versionable aspect is not present + ContentWriter contentWriter = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.putContent("content update one"); + assertFalse(this.policyFired); + + // Now check that the policy is fired when the versionable aspect is present + this.nodeService.addAspect(this.contentNodeRef, ContentModel.ASPECT_VERSIONABLE, null); + ContentWriter contentWriter2 = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + contentWriter2.putContent("content update two"); + assertTrue(this.policyFired); + this.policyFired = false; + + // Check that the policy is not fired when using a non updating content writer + ContentWriter contentWriter3 = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false); + contentWriter3.putContent("content update three"); + assertFalse(this.policyFired); + } + + public void onContentUpdateBehaviourTest(NodeRef nodeRef) + { + assertEquals(this.contentNodeRef, nodeRef); + assertTrue(this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE)); + this.policyFired = true; + } + + public void testTempWrite() throws Exception + { + // get a temporary writer + ContentWriter writer1 = contentService.getTempWriter(); + // and another + ContentWriter writer2 = contentService.getTempWriter(); + + // check + assertNotSame("Temp URLs must be different", + writer1.getContentUrl(), writer2.getContentUrl()); + } + + /** + * Tests the automatic updating of nodes' content URLs + */ + public void testUpdatingWrite() throws Exception + { + // check that the content URL property has not been set + ContentData contentData = (ContentData) nodeService.getProperty( + contentNodeRef, + ContentModel.PROP_CONTENT); + assertNull("Content URL should be null", contentData.getContentUrl()); + + // before the content is written, there should not be any reader available + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNull("No reader should be available for new node", reader); + + // get the writer + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + assertNotNull("No writer received", writer); + // write some content directly + writer.putContent(SOME_CONTENT); + + // make sure that we can't reuse the writer + try + { + writer.putContent("DEF"); + fail("Failed to prevent repeated use of the content writer"); + } + catch (ContentIOException e) + { + // expected + } + + // check that there is a reader available + reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNotNull("No reader available for node", reader); + String contentCheck = reader.getContentString(); + assertEquals("Content fetched doesn't match that written", SOME_CONTENT, contentCheck); + + // check that the content data was set + contentData = (ContentData) nodeService.getProperty( + contentNodeRef, + ContentModel.PROP_CONTENT); + assertNotNull("Content data not set", contentData); + assertEquals("Mismatched URL between writer and node", + writer.getContentUrl(), contentData.getContentUrl()); + + // check that the content size was set + assertEquals("Reader content length and node content length different", + reader.getSize(), contentData.getSize()); + + // check that the mimetype was set + assertEquals("Mimetype not set on content data", "text/plain", contentData.getMimetype()); + // check encoding + assertEquals("Encoding not set", "UTF-16", contentData.getEncoding()); + } + + /** + * Checks that multiple writes can occur to the same node outside of any transactions. + *

    + * It is only when the streams are closed that the node is updated. + */ + public void testConcurrentWritesNoTxn() throws Exception + { + // ensure that the transaction is ended - ofcourse, we need to force a commit + setComplete(); + endTransaction(); + + ContentWriter writer1 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + ContentWriter writer2 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + ContentWriter writer3 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + + writer1.putContent("writer1 wrote this"); + writer2.putContent("writer2 wrote this"); + writer3.putContent("writer3 wrote this"); + + // get the content + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + String contentCheck = reader.getContentString(); + assertEquals("Content check failed", "writer3 wrote this", contentCheck); + } + + public void testConcurrentWritesWithSingleTxn() throws Exception + { + // want to operate in a user transaction + setComplete(); + endTransaction(); + + UserTransaction txn = getUserTransaction(); + txn.begin(); + txn.setRollbackOnly(); + + ContentWriter writer1 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + ContentWriter writer2 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + ContentWriter writer3 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + + writer1.putContent("writer1 wrote this"); + writer2.putContent("writer2 wrote this"); + writer3.putContent("writer3 wrote this"); + + // get the content + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + String contentCheck = reader.getContentString(); + assertEquals("Content check failed", "writer3 wrote this", contentCheck); + + try + { + txn.commit(); + fail("Transaction has been marked for rollback"); + } + catch (RollbackException e) + { + // expected + } + + // rollback and check that the content has 'disappeared' + txn.rollback(); + reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNull("Transaction was rolled back - no content should be visible", reader); + } + + public synchronized void testConcurrentWritesWithMultipleTxns() throws Exception + { + // commit node so that threads can see node + setComplete(); + endTransaction(); + + UserTransaction txn = getUserTransaction(); + txn.begin(); + + // ensure that there is no content to read on the node + ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNull("Reader should not be available", reader); + + ContentWriter threadWriter = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + String threadContent = "Thread content"; + WriteThread thread = new WriteThread(threadWriter, threadContent); + // kick off thread + thread.start(); + // wait for thread to get to its wait points + while (!thread.isWaiting()) + { + wait(10); + } + + // write to the content + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + writer.putContent(SOME_CONTENT); + + // fire thread up again + synchronized(threadWriter) + { + threadWriter.notifyAll(); + } + // thread is released - but we have to wait for it to complete + while (!thread.isDone()) + { + wait(10); + } + + // the thread has finished and has committed its changes - check the visibility + reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNotNull("Reader should now be available", reader); + String checkContent = reader.getContentString(); + assertEquals("Content check failed", SOME_CONTENT, checkContent); + + // rollback the txn + txn.rollback(); + + // check content has taken on thread's content + reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertNotNull("Reader should now be available", reader); + checkContent = reader.getContentString(); + assertEquals("Content check failed", threadContent, checkContent); + } + + public void testTransformation() throws Exception + { + // get a regular writer + ContentWriter writer = contentService.getTempWriter(); + writer.setMimetype("text/xml"); + // write some stuff + String content = ""; + writer.putContent(content); + // get a reader onto the content + ContentReader reader = writer.getReader(); + + // get a new writer for the transformation + writer = contentService.getTempWriter(); + writer.setMimetype("audio/x-wav"); // no such conversion possible + try + { + contentService.transform(reader, writer); + fail("Transformation attempted with invalid mimetype"); + } + catch (NoTransformerException e) + { + // expected + } + writer.setMimetype("text/plain"); + contentService.transform(reader, writer); + // get the content from the writer + reader = writer.getReader(); + assertEquals("Mimetype of target reader incorrect", + writer.getMimetype(), reader.getMimetype()); + String contentCheck = reader.getContentString(); + assertEquals("Content check failed", content, contentCheck); + } + + /** + * Writes some content to the writer's output stream and then aquires + * a lock on the writer, waits until notified and then closes the + * output stream before terminating. + *

    + * When firing thread up, be sure to call notify on the + * writer in order to let the thread run to completion. + */ + private class WriteThread extends Thread + { + private ContentWriter writer; + private String content; + private boolean isWaiting; + private boolean isDone; + + public WriteThread(ContentWriter writer, String content) + { + this.writer = writer; + this.content = content; + isWaiting = false; + isDone = false; + } + + public boolean isWaiting() + { + return isWaiting; + } + + public boolean isDone() + { + return isDone; + } + + public void run() + { + isWaiting = false; + isDone = false; + UserTransaction txn = getUserTransaction(); + OutputStream os = writer.getContentOutputStream(); + try + { + txn.begin(); // not testing transactions - this is not a safe pattern + // put the content + if (writer.getEncoding() == null) + { + os.write(content.getBytes()); + } + else + { + os.write(content.getBytes(writer.getEncoding())); + } + synchronized (writer) + { + isWaiting = true; + writer.wait(); // wait until notified + } + os.close(); + os = null; + txn.commit(); + } + catch (Throwable e) + { + try {txn.rollback(); } catch (Exception ee) {} + e.printStackTrace(); + throw new RuntimeException("Failed writing to output stream for writer: " + writer, e); + } + finally + { + if (os != null) + { + try { os.close(); } catch (IOException e) {} + } + isDone = true; + } + } + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentReader.java b/source/java/org/alfresco/repo/content/filestore/FileContentReader.java new file mode 100644 index 0000000000..333eea0fee --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/FileContentReader.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.filestore; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.text.MessageFormat; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.AbstractContentReader; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.RandomAccessContent; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides direct access to a local file. + *

    + * This class does not provide remote access to the file. + * + * @author Derek Hulley + */ +public class FileContentReader extends AbstractContentReader implements RandomAccessContent +{ + private static final Log logger = LogFactory.getLog(FileContentReader.class); + + private File file; + + /** + * Checks the existing reader provided and replaces it with a reader onto some + * fake content if required. If the existing reader is invalid, an debug message + * will be logged under this classname category. + *

    + * It is a convenience method that clients can use to cheaply get a reader that + * is valid, regardless of whether the initial reader is valid. + * + * @param existingReader a potentially valid reader + * @param msgTemplate the template message that will used to format the final fake content + * @param args arguments to put into the fake content + * @return Returns a the existing reader or a new reader onto some generated text content + */ + public static ContentReader getSafeContentReader(ContentReader existingReader, String msgTemplate, Object ... args) + { + ContentReader reader = existingReader; + if (existingReader == null || !existingReader.exists()) + { + // the content was never written to the node or the underlying content is missing + String fakeContent = MessageFormat.format(msgTemplate, args); + + // log it + if (logger.isDebugEnabled()) + { + logger.debug(fakeContent); + } + + // fake the content + File tempFile = TempFileProvider.createTempFile("getSafeContentReader_", ".txt"); + ContentWriter writer = new FileContentWriter(tempFile); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(fakeContent); + // grab the reader from the temp writer + reader = writer.getReader(); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created safe content reader: \n" + + " existing reader: " + existingReader + "\n" + + " safe reader: " + reader); + } + return reader; + } + + /** + * Constructor that builds a URL based on the absolute path of the file. + * + * @param file the file for reading. This will most likely be directly + * related to the content URL. + */ + public FileContentReader(File file) + { + this(file, FileContentStore.STORE_PROTOCOL + file.getAbsolutePath()); + } + + /** + * Constructor that explicitely sets the URL that the reader represents. + * + * @param file the file for reading. This will most likely be directly + * related to the content URL. + * @param url the relative url that the reader represents + */ + public FileContentReader(File file, String url) + { + super(url); + + this.file = file; + } + + /** + * @return Returns the file that this reader accesses + */ + public File getFile() + { + return file; + } + + public boolean exists() + { + return file.exists(); + } + + /** + * @see File#length() + */ + public long getSize() + { + if (!exists()) + { + return 0L; + } + else + { + return file.length(); + } + } + + /** + * @see File#lastModified() + */ + public long getLastModified() + { + if (!exists()) + { + return 0L; + } + else + { + return file.lastModified(); + } + } + + /** + * The URL of the write is known from the start and this method contract states + * that no consideration needs to be taken w.r.t. the stream state. + */ + @Override + protected ContentReader createReader() throws ContentIOException + { + return new FileContentReader(this.file, getContentUrl()); + } + + @Override + protected ReadableByteChannel getDirectReadableChannel() throws ContentIOException + { + try + { + // the file must exist + if (!file.exists()) + { + throw new IOException("File does not exist"); + } + // create the channel + RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // won't create it + FileChannel channel = randomAccessFile.getChannel(); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Opened channel to file: " + file); + } + return channel; + } + catch (Throwable e) + { + throw new ContentIOException("Failed to open file channel: " + this, e); + } + } + + /** + * @param directChannel a file channel + */ + @Override + protected ReadableByteChannel getCallbackReadableChannel( + ReadableByteChannel directChannel, + List listeners) throws ContentIOException + { + if (!(directChannel instanceof FileChannel)) + { + throw new AlfrescoRuntimeException("Expected read channel to be a file channel"); + } + FileChannel fileChannel = (FileChannel) directChannel; + // wrap it + FileChannel callbackChannel = new CallbackFileChannel(fileChannel, listeners); + // done + return callbackChannel; + } + + /** + * @return Returns false as this is a reader + */ + public boolean canWrite() + { + return false; // we only allow reading + } + + public FileChannel getChannel() throws ContentIOException + { + // go through the super classes to ensure that all concurrency conditions + // and listeners are satisfied + return (FileChannel) super.getReadableChannel(); + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java new file mode 100644 index 0000000000..a47415c9ab --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.filestore; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.AbstractContentStore; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides a store of node content directly to the file system. + *

    + * The file names obey, as they must, the URL naming convention + * as specified in the {@link org.alfresco.repo.content.ContentStore}. + * + * @author Derek Hulley + */ +public class FileContentStore extends AbstractContentStore +{ + private static final Log logger = LogFactory.getLog(FileContentStore.class); + + private File rootDirectory; + private String rootAbsolutePath; + + /** + * @param rootDirectory the root under which files will be stored. The + * directory will be created if it does not exist. + */ + public FileContentStore(String rootDirectoryStr) + { + rootDirectory = new File(rootDirectoryStr); + if (!rootDirectory.exists()) + { + if (!rootDirectory.mkdirs()) + { + throw new ContentIOException("Failed to create store root: " + rootDirectory, null); + } + } + rootDirectory = rootDirectory.getAbsoluteFile(); + rootAbsolutePath = rootDirectory.getAbsolutePath(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(36); + sb.append("FileContentStore") + .append("[ root=").append(rootDirectory) + .append("]"); + return sb.toString(); + } + + /** + * Generates a new URL and file appropriate to it. + * + * @return Returns a new and unique file + * @throws IOException if the file or parent directories couldn't be created + */ + private File createNewFile() throws IOException + { + String contentUrl = createNewUrl(); + return createNewFile(contentUrl); + } + + /** + * Creates a file for the specifically provided content URL. The URL may + * not already be in use. + *

    + * The store prefix is stripped off the URL and the rest of the URL + * used directly to create a file. + * + * @param newContentUrl the specific URL to use, which may not be in use + * @return Returns a new and unique file + * @throws IOException if the file or parent directories couldn't be created or + * if the URL is already in use. + */ + public File createNewFile(String newContentUrl) throws IOException + { + File file = makeFile(newContentUrl); + + // create the directory, if it doesn't exist + File dir = file.getParentFile(); + if (!dir.exists()) + { + dir.mkdirs(); + } + + // create a new, empty file + boolean created = file.createNewFile(); + if (!created) + { + throw new ContentIOException( + "When specifying a URL for new content, the URL may not be in use already. \n" + + " store: " + this + "\n" + + " new URL: " + newContentUrl); + } + + // done + return file; + } + + /** + * Takes the file absolute path, strips off the root path of the store + * and appends the store URL prefix. + * + * @param file the file from which to create the URL + * @return Returns the equivalent content URL + * @throws Exception + */ + private String makeContentUrl(File file) + { + String path = file.getAbsolutePath(); + // check if it belongs to this store + if (!path.startsWith(rootAbsolutePath)) + { + throw new AlfrescoRuntimeException( + "File does not fall below the store's root: \n" + + " file: " + file + "\n" + + " store: " + this); + } + // strip off the file separator char, if present + int index = rootAbsolutePath.length(); + if (path.charAt(index) == File.separatorChar) + { + index++; + } + // strip off the root path and adds the protocol prefix + String url = AbstractContentStore.STORE_PROTOCOL + path.substring(index); + // replace '\' with '/' so that URLs are consistent across all filesystems + url = url.replace('\\', '/'); + // done + return url; + } + + /** + * Creates a file from the given relative URL. The URL must start with + * the required {@link FileContentStore#STORE_PROTOCOL protocol prefix}. + * + * @param contentUrl the content URL including the protocol prefix + * @return Returns a file representing the URL - the file may or may not + * exist + * + * @see #checkUrl(String) + */ + private File makeFile(String contentUrl) + { + // take just the part after the protocol + String relativeUrl = getRelativePart(contentUrl); + // get the file + File file = new File(rootDirectory, relativeUrl); + // done + return file; + } + + /** + * Performs a direct check against the file for its existence. + */ + @Override + public boolean exists(String contentUrl) throws ContentIOException + { + File file = makeFile(contentUrl); + return file.exists(); + } + + /** + * This implementation requires that the URL start with + * {@link FileContentStore#STORE_PROTOCOL }. + */ + public ContentReader getReader(String contentUrl) + { + try + { + File file = makeFile(contentUrl); + FileContentReader reader = new FileContentReader(file, contentUrl); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created content reader: \n" + + " url: " + contentUrl + "\n" + + " file: " + file + "\n" + + " reader: " + reader); + } + return reader; + } + catch (Throwable e) + { + throw new ContentIOException("Failed to get reader for URL: " + contentUrl, e); + } + } + + /** + * @return Returns a writer onto a location based on the date + */ + public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) + { + try + { + File file = null; + String contentUrl = null; + if (newContentUrl == null) // a specific URL was not supplied + { + // get a new file with a new URL + file = createNewFile(); + // make a URL + contentUrl = makeContentUrl(file); + } + else // the URL has been given + { + file = createNewFile(newContentUrl); + contentUrl = newContentUrl; + } + // create the writer + FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Created content writer: \n" + + " writer: " + writer); + } + return writer; + } + catch (IOException e) + { + throw new ContentIOException("Failed to get writer", e); + } + } + + public Set getUrls() + { + // recursively get all files within the root + Set contentUrls = new HashSet(1000); + getUrls(rootDirectory, contentUrls); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Listed all content URLS: \n" + + " store: " + this + "\n" + + " count: " + contentUrls.size()); + } + return contentUrls; + } + + /** + * @param directory the current directory to get the files from + * @param contentUrls the list of current content URLs to add to + * @return Returns a list of all files within the given directory and all subdirectories + */ + private void getUrls(File directory, Set contentUrls) + { + File[] files = directory.listFiles(); + if (files == null) + { + // the directory has disappeared + throw new ContentIOException("Failed list files in folder: " + directory); + } + for (File file : files) + { + if (file.isDirectory()) + { + // we have a subdirectory - recurse + getUrls(file, contentUrls); + } + else + { + // found a file - create the URL + String contentUrl = makeContentUrl(file); + contentUrls.add(contentUrl); + } + } + } + + /** + * Attempts to delete the content. The actual deletion is optional on the interface + * so it just returns the success or failure of the underlying delete. + */ + public boolean delete(String contentUrl) throws ContentIOException + { + // ignore files that don't exist + File file = makeFile(contentUrl); + if (!file.exists()) + { + return true; + } + // attempt to delete the file directly + boolean deleted = file.delete(); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Delete content directly: \n" + + " store: " + this + "\n" + + " url: " + contentUrl); + } + return deleted; + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java new file mode 100644 index 0000000000..d10412f587 --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.filestore; + +import java.io.File; + +import org.alfresco.repo.content.AbstractContentReadWriteTest; +import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.util.TempFileProvider; + +/** + * Tests read and write functionality for the store. + * + * @see org.alfresco.repo.content.filestore.FileContentStore + * + * @author Derek Hulley + */ +public class FileContentStoreTest extends AbstractContentReadWriteTest +{ + private FileContentStore store; + + @Override + public void setUp() throws Exception + { + super.setUp(); + + // create a store that uses a subdirectory of the temp directory + File tempDir = TempFileProvider.getTempDir(); + store = new FileContentStore( + tempDir.getAbsolutePath() + + File.separatorChar + + getName()); + } + + @Override + protected ContentStore getStore() + { + return store; + } + + public void testGetSafeContentReader() throws Exception + { + String template = "ABC {0}{1}"; + String arg0 = "DEF"; + String arg1 = "123"; + String fakeContent = "ABC DEF123"; + + // get a good reader + ContentReader reader = getReader(); + assertFalse("No content has been written to the URL yet", reader.exists()); + + // now create a file for it + File file = store.createNewFile(reader.getContentUrl()); + assertTrue("File store did not connect new file", file.exists()); + assertTrue("Reader did not detect creation of the underlying file", reader.exists()); + + // remove the underlying content + file.delete(); + assertFalse("File not missing", file.exists()); + assertFalse("Reader doesn't show missing content", reader.exists()); + + // make a safe reader + ContentReader safeReader = FileContentReader.getSafeContentReader(reader, template, arg0, arg1); + // check it + assertTrue("Fake content doesn't exist", safeReader.exists()); + assertEquals("Fake content incorrect", fakeContent, safeReader.getContentString()); + assertEquals("Fake mimetype incorrect", MimetypeMap.MIMETYPE_TEXT_PLAIN, safeReader.getMimetype()); + assertEquals("Fake encoding incorrect", "UTF-8", safeReader.getEncoding()); + + // now repeat with a null reader + reader = null; + safeReader = FileContentReader.getSafeContentReader(reader, template, arg0, arg1); + // check it + assertTrue("Fake content doesn't exist", safeReader.exists()); + assertEquals("Fake content incorrect", fakeContent, safeReader.getContentString()); + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java b/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java new file mode 100644 index 0000000000..753d785c55 --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/FileContentWriter.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.filestore; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.AbstractContentWriter; +import org.alfresco.repo.content.RandomAccessContent; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides direct access to a local file. + *

    + * This class does not provide remote access to the file. + * + * @author Derek Hulley + */ +public class FileContentWriter extends AbstractContentWriter implements RandomAccessContent +{ + private static final Log logger = LogFactory.getLog(FileContentWriter.class); + + private File file; + + /** + * Constructor that builds a URL based on the absolute path of the file. + * + * @param file the file for writing. This will most likely be directly + * related to the content URL. + */ + public FileContentWriter(File file) + { + this( + file, + FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(), + null); + } + + /** + * Constructor that builds a URL based on the absolute path of the file. + * + * @param file the file for writing. This will most likely be directly + * related to the content URL. + * @param existingContentReader a reader of a previous version of this content + */ + public FileContentWriter(File file, ContentReader existingContentReader) + { + this( + file, + FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(), + existingContentReader); + } + + /** + * Constructor that explicitely sets the URL that the reader represents. + * + * @param file the file for writing. This will most likely be directly + * related to the content URL. + * @param url the relative url that the reader represents + * @param existingContentReader a reader of a previous version of this content + */ + public FileContentWriter(File file, String url, ContentReader existingContentReader) + { + super(url, existingContentReader); + + this.file = file; + } + + /** + * @return Returns the file that this writer accesses + */ + public File getFile() + { + return file; + } + + /** + * @return Returns the size of the underlying file or + */ + public long getSize() + { + if (file == null) + return 0L; + else if (!file.exists()) + return 0L; + else + return file.length(); + } + + /** + * The URL of the write is known from the start and this method contract states + * that no consideration needs to be taken w.r.t. the stream state. + */ + @Override + protected ContentReader createReader() throws ContentIOException + { + return new FileContentReader(this.file, getContentUrl()); + } + + @Override + protected WritableByteChannel getDirectWritableChannel() throws ContentIOException + { + try + { + // we may not write to an existing file - EVER!! + if (file.exists() && file.length() > 0) + { + throw new IOException("File exists - overwriting not allowed"); + } + // create the channel + RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); // will create it + FileChannel channel = randomAccessFile.getChannel(); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Opened channel to file: " + file); + } + return channel; + } + catch (Throwable e) + { + throw new ContentIOException("Failed to open file channel: " + this, e); + } + } + + /** + * @param directChannel a file channel + */ + @Override + protected WritableByteChannel getCallbackWritableChannel( + WritableByteChannel directChannel, + List listeners) throws ContentIOException + { + if (!(directChannel instanceof FileChannel)) + { + throw new AlfrescoRuntimeException("Expected write channel to be a file channel"); + } + FileChannel fileChannel = (FileChannel) directChannel; + // wrap it + FileChannel callbackChannel = new CallbackFileChannel(fileChannel, listeners); + // done + return callbackChannel; + } + + /** + * @return Returns true always + */ + public boolean canWrite() + { + return true; // this is a writer + } + + public FileChannel getChannel() throws ContentIOException + { + /* + * By calling this method, clients indicate that they wish to make random + * changes to the file. It is possible that the client might only want + * to update a tiny proportion of the file - or even none of it. Either + * way, the file must be as whole and complete as before it was accessed. + */ + + // go through the super classes to ensure that all concurrency conditions + // and listeners are satisfied + FileChannel channel = (FileChannel) super.getWritableChannel(); + // random access means that the the new content's starting point must be + // that of the existing content + ContentReader existingContentReader = getExistingContentReader(); + if (existingContentReader != null) + { + ReadableByteChannel existingContentChannel = existingContentReader.getReadableChannel(); + long existingContentLength = existingContentReader.getSize(); + // copy the existing content + try + { + channel.transferFrom(existingContentChannel, 0, existingContentLength); + // copy complete + if (logger.isDebugEnabled()) + { + logger.debug("Copied content for random access: \n" + + " writer: " + this + "\n" + + " existing: " + existingContentReader); + } + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy from existing content to enable random access: \n" + + " writer: " + this + "\n" + + " existing: " + existingContentReader, + e); + } + finally + { + try { existingContentChannel.close(); } catch (IOException e) {} + } + } + // the file is now available for random access + return channel; + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileIOTest.java b/source/java/org/alfresco/repo/content/filestore/FileIOTest.java new file mode 100644 index 0000000000..0d0a4dd6d9 --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/FileIOTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.filestore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import junit.framework.TestCase; + +/** + * Some tests to check out the java.lang.nio functionality + * + * @author Derek Hulley + */ +public class FileIOTest extends TestCase +{ + private static final String TEST_CONTENT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + private File file; + + public FileIOTest(String name) + { + super(name); + } + + public void setUp() throws Exception + { + file = File.createTempFile(getName(), ".txt"); + OutputStream os = new FileOutputStream(file); + os.write(TEST_CONTENT.getBytes()); + os.flush(); + os.close(); + } + + /** + * Attempt to read the same file using multiple channels concurrently + */ + public void testConcurrentFileReads() throws Exception + { + // open the file for a read + FileInputStream isA = new FileInputStream(file); + FileInputStream isB = new FileInputStream(file); + + // get the channels + FileChannel channelA = isA.getChannel(); + FileChannel channelB = isB.getChannel(); + + // buffers for reading + ByteBuffer bufferA = ByteBuffer.allocate(10); + ByteBuffer bufferB = ByteBuffer.allocate(10); + + // read file into both buffers + int countA = 0; + int countB = 0; + do + { + countA = channelA.read((ByteBuffer)bufferA.clear()); + countB = channelB.read((ByteBuffer)bufferB.clear()); + assertEquals("Should read same number of bytes", countA, countB); + } while (countA > 6); + + // both buffers should be at the same marker 6 + assertEquals("BufferA marker incorrect", 6, bufferA.position()); + assertEquals("BufferB marker incorrect", 6, bufferB.position()); + } + + public void testConcurrentReadWrite() throws Exception + { + // open file for a read + FileInputStream isRead = new FileInputStream(file); + // open file for write + FileOutputStream osWrite = new FileOutputStream(file); + + // get channels + FileChannel channelRead = isRead.getChannel(); + FileChannel channelWrite = osWrite.getChannel(); + + // buffers + ByteBuffer bufferRead = ByteBuffer.allocate(26); + ByteBuffer bufferWrite = ByteBuffer.wrap(TEST_CONTENT.getBytes()); + + // read - nothing will be read + int countRead = channelRead.read(bufferRead); + assertEquals("Expected nothing to be read", -1, countRead); + // write + int countWrite = channelWrite.write(bufferWrite); + assertEquals("Not all characters written", 26, countWrite); + + // close the write side + channelWrite.close(); + + // reread + countRead = channelRead.read(bufferRead); + assertEquals("Expected full read", 26, countRead); + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracter.java new file mode 100644 index 0000000000..8d0bbc6bb3 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracter.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.namespace.QName; + +/** + * + * @author Jesper Steen Møller + */ +abstract public class AbstractMetadataExtracter implements MetadataExtracter +{ + + private Set mimetypes; + private double reliability; + private long extractionTime; + + protected AbstractMetadataExtracter(String mimetype, double reliability, long extractionTime) + { + this.mimetypes = Collections.singleton(mimetype); + this.reliability = reliability; + this.extractionTime = extractionTime; + } + + protected AbstractMetadataExtracter(Set mimetypes, double reliability, long extractionTime) + { + this.mimetypes = mimetypes; + this.reliability = reliability; + this.extractionTime = extractionTime; + } + + public double getReliability(String sourceMimetype) + { + if (mimetypes.contains(sourceMimetype)) + return reliability; + else + return 0.0; + } + + public long getExtractionTime() + { + return extractionTime; + } + + /** + * Examines a value or string for nulls and adds it to the map (if + * non-empty) + * + * @param prop Alfresco's ContentModel.PROP_ to set. + * @param value Value to set it to + * @param destination Map into which to set it + * @return true, if set, false otherwise + */ + protected boolean trimPut(QName prop, Object value, Map destination) + { + if (value == null) + return false; + if (value instanceof String) + { + String svalue = ((String) value).trim(); + if (svalue.length() > 0) + { + destination.put(prop, svalue); + return true; + } + return false; + } + else if (value instanceof Serializable) + { + destination.put(prop, (Serializable) value); + } + else + { + destination.put(prop, value.toString()); + } + return true; + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracterTest.java b/source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracterTest.java new file mode 100644 index 0000000000..749780003b --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/AbstractMetadataExtracterTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Serializable; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.TempFileProvider; + +/** + * Provides a base set of tests for testing + * {@link org.alfresco.repo.content.metadata.MetadataExtracter} implementations. + * + * @author Jesper Steen Møller + */ +public abstract class AbstractMetadataExtracterTest extends BaseSpringTest +{ + protected static final String QUICK_TITLE = "The quick brown fox jumps over the lazy dog"; + protected static final String QUICK_DESCRIPTION = "Gym class featuring a brown fox and lazy dog"; + protected static final String QUICK_CREATOR = "Nevin Nollop"; + protected static final String[] QUICK_WORDS = new String[] { "quick", "brown", "fox", "jumps", "lazy", "dog" }; + + protected MimetypeMap mimetypeMap; + protected MetadataExtracter transformer; + + public final void setMimetypeMap(MimetypeMap mimetypeMap) + { + this.mimetypeMap = mimetypeMap; + } + + protected abstract MetadataExtracter getExtracter(); + + /** + * Ensures that the temp locations are cleaned out before the tests start + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + // perform a little cleaning up + long now = System.currentTimeMillis(); + TempFileProvider.TempFileCleanerJob.removeFiles(now); + } + + /** + * Check that all objects are present + */ + public void testSetUp() throws Exception + { + assertNotNull("MimetypeMap not present", mimetypeMap); + // check that the quick resources are available + File sourceFile = AbstractMetadataExtracterTest.loadQuickTestFile("txt"); + assertNotNull("quick.* files should be available from Tests", sourceFile); + } + + /** + * Helper method to load one of the "The quick brown fox" files from the + * classpath. + * + * @param extension the extension of the file required + * @return Returns a test resource loaded from the classpath or + * null if no resource could be found. + * @throws IOException + */ + public static File loadQuickTestFile(String extension) throws IOException + { + URL url = AbstractMetadataExtracterTest.class.getClassLoader().getResource("quick/quick." + extension); + if (url == null) + { + return null; + } + File file = new File(url.getFile()); + if (!file.exists()) + { + return null; + } + return file; + } + + public Map extractFromExtension(String ext, String mimetype) throws Exception + { + Map destination = new HashMap(); + + // attempt to get a source file for each mimetype + File sourceFile = AbstractMetadataExtracterTest.loadQuickTestFile(ext); + if (sourceFile == null) + { + throw new FileNotFoundException("No quick." + ext + " file found for test"); + } + + // construct a reader onto the source file + ContentReader sourceReader = new FileContentReader(sourceFile); + sourceReader.setMimetype(mimetype); + getExtracter().extract(sourceReader, destination); + return destination; + } + + public void testCommonMetadata(Map destination) + { + assertEquals(QUICK_TITLE, destination.get(ContentModel.PROP_TITLE)); + assertEquals(QUICK_DESCRIPTION, destination.get(ContentModel.PROP_DESCRIPTION)); + assertEquals(QUICK_CREATOR, destination.get(ContentModel.PROP_CREATOR)); + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracter.java new file mode 100644 index 0000000000..5c0ce275ff --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracter.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.text.ChangedCharSetException; +import javax.swing.text.MutableAttributeSet; +import javax.swing.text.html.HTML; +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.html.parser.ParserDelegator; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * @author Jesper Steen Møller + */ +public class HtmlMetadataExtracter extends AbstractMetadataExtracter +{ + + private static final Log logger = LogFactory.getLog(HtmlMetadataExtracter.class); + + public HtmlMetadataExtracter() + { + super(MimetypeMap.MIMETYPE_HTML, 1.0, 1000); + } + + public void extract(ContentReader reader, Map destination) throws ContentIOException + { + final Map tempDestination = new HashMap(); + try + { + HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback() + { + StringBuffer title = null; + boolean inHead = false; + + public void handleText(char[] data, int pos) + { + if (title != null) + { + title.append(data); + } + } + + public void handleComment(char[] data, int pos) + { + // Perhaps sniff for Office 9+ metadata in here? + } + + public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) + { + if (HTML.Tag.HEAD.equals(t)) + { + inHead = true; + } + else if (HTML.Tag.TITLE.equals(t) && inHead) + { + title = new StringBuffer(); + } + else + handleSimpleTag(t, a, pos); + } + + public void handleEndTag(HTML.Tag t, int pos) + { + if (HTML.Tag.HEAD.equals(t)) + { + inHead = false; + } + else if (HTML.Tag.TITLE.equals(t)) + { + trimPut(ContentModel.PROP_TITLE, title.toString(), tempDestination); + title = null; + } + } + + public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) + { + if (HTML.Tag.META.equals(t)) + { + Object nameO = a.getAttribute(HTML.Attribute.NAME); + Object valueO = a.getAttribute(HTML.Attribute.CONTENT); + if (nameO == null || valueO == null) + return; + + String name = nameO.toString(); + + if (name.equalsIgnoreCase("creator") || name.equalsIgnoreCase("author") + || name.equalsIgnoreCase("dc.creator")) + { + trimPut(ContentModel.PROP_CREATOR, valueO, tempDestination); + } + if (name.equalsIgnoreCase("description") || name.equalsIgnoreCase("dc.description")) + { + trimPut(ContentModel.PROP_DESCRIPTION, valueO, tempDestination); + } + } + } + + public void handleError(String errorMsg, int pos) + { + } + }; + + String charsetGuess = "UTF-8"; + int tries = 0; + while (tries < 3) + { + tempDestination.clear(); + Reader r = null; + InputStream cis = null; + try + { + cis = reader.getContentInputStream(); + // TODO: for now, use default charset; we should attempt to map from html meta-data + r = new InputStreamReader(cis); + HTMLEditorKit.Parser parser = new ParserDelegator(); + parser.parse(r, callback, tries > 0); + destination.putAll(tempDestination); + break; + } + catch (ChangedCharSetException ccse) + { + tries++; + charsetGuess = ccse.getCharSetSpec(); + int begin = charsetGuess.indexOf("charset="); + if (begin > 0) + charsetGuess = charsetGuess.substring(begin + 8, charsetGuess.length()); + reader = reader.getReader(); + } + finally + { + if (r != null) + r.close(); + if (cis != null) + cis.close(); + } + } + } + catch (IOException e) + { + throw new ContentIOException("HTML metadata extraction failed: \n" + " reader: " + reader, e); + } + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracterTest.java b/source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracterTest.java new file mode 100644 index 0000000000..49acfb2026 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/HtmlMetadataExtracterTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.OfficeMetadataExtracter + * @author Jesper Steen Møller + */ +public class HtmlMetadataExtracterTest extends AbstractMetadataExtracterTest +{ + private static final Log logger = LogFactory.getLog(HtmlMetadataExtracterTest.class); + private MetadataExtracter extracter; + + public void onSetUpInTransaction() throws Exception + { + extracter = new HtmlMetadataExtracter(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected MetadataExtracter getExtracter() + { + return extracter; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype text should not be supported", 0.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_HTML); + assertEquals("HTML should be supported", 1.0, reliability); + } + + public void testHtmlExtraction() throws Exception + { + testCommonMetadata(extractFromExtension("html", MimetypeMap.MIMETYPE_HTML)); + } + +} diff --git a/source/java/org/alfresco/repo/content/metadata/MP3MetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/MP3MetadataExtracter.java new file mode 100644 index 0000000000..93d3580752 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/MP3MetadataExtracter.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.farng.mp3.AbstractMP3FragmentBody; +import org.farng.mp3.MP3File; +import org.farng.mp3.TagException; +import org.farng.mp3.id3.AbstractID3v2; +import org.farng.mp3.id3.AbstractID3v2Frame; +import org.farng.mp3.id3.ID3v1; +import org.farng.mp3.lyrics3.AbstractLyrics3; +import org.farng.mp3.lyrics3.Lyrics3v2; +import org.farng.mp3.lyrics3.Lyrics3v2Field; + +/** + * @author Roy Wetherall + */ +public class MP3MetadataExtracter extends AbstractMetadataExtracter +{ + private static final QName PROP_ALBUM_TITLE = QName.createQName("{music}albumTitle"); + private static final QName PROP_SONG_TITLE = QName.createQName("{music}songTitle");; + private static final QName PROP_ARTIST = QName.createQName("{music}artist");; + private static final QName PROP_COMMENT = QName.createQName("{music}comment");; + private static final QName PROP_YEAR_RELEASED = QName.createQName("{music}yearReleased");; + private static final QName PROP_TRACK_NUMBER = QName.createQName("{music}trackNumber");; + private static final QName PROP_GENRE = QName.createQName("{music}genre");; + private static final QName PROP_COMPOSER = QName.createQName("{music}composer");; + private static final QName PROP_LYRICS = QName.createQName("{music}lyrics");; + + public MP3MetadataExtracter() + { + super(MimetypeMap.MIMETYPE_MP3, 1.0, 1000); + } + + /** + * @see org.alfresco.repo.content.metadata.MetadataExtracter#extract(org.alfresco.service.cmr.repository.ContentReader, java.util.Map) + */ + public void extract(ContentReader reader, + Map destination) throws ContentIOException + { + try + { + Map props = new HashMap(); + + // Create a temp file + File tempFile = File.createTempFile(GUID.generate(), ".tmp"); + try + { + reader.getContent(tempFile); + + // Create the MP3 object from the file + MP3File mp3File = new MP3File(tempFile); + + ID3v1 id3v1 = mp3File.getID3v1Tag(); + if (id3v1 != null) + { + setTagValue(props, PROP_ALBUM_TITLE, id3v1.getAlbum()); + setTagValue(props, PROP_SONG_TITLE, id3v1.getTitle()); + setTagValue(props, PROP_ARTIST, id3v1.getArtist()); + setTagValue(props, PROP_COMMENT, id3v1.getComment()); + setTagValue(props, PROP_YEAR_RELEASED, id3v1.getYear()); + + // TODO sort out the genre + //setTagValue(props, MusicModel.PROP_GENRE, id3v1.getGenre()); + + // TODO sort out the size + //setTagValue(props, MusicModel.PROP_SIZE, id3v1.getSize()); + } + + AbstractID3v2 id3v2 = mp3File.getID3v2Tag(); + if (id3v2 != null) + { + setTagValue(props, PROP_SONG_TITLE, getID3V2Value(id3v2, "TIT2")); + setTagValue(props, PROP_ARTIST, getID3V2Value(id3v2, "TPE1")); + setTagValue(props, PROP_ALBUM_TITLE, getID3V2Value(id3v2, "TALB")); + setTagValue(props, PROP_YEAR_RELEASED, getID3V2Value(id3v2, "TDRC")); + setTagValue(props, PROP_COMMENT, getID3V2Value(id3v2, "COMM")); + setTagValue(props, PROP_TRACK_NUMBER, getID3V2Value(id3v2, "TRCK")); + setTagValue(props, PROP_GENRE, getID3V2Value(id3v2, "TCON")); + setTagValue(props, PROP_COMPOSER, getID3V2Value(id3v2, "TCOM")); + + // TODO sort out the lyrics + //System.out.println("Lyrics: " + getID3V2Value(id3v2, "SYLT")); + //System.out.println("Lyrics: " + getID3V2Value(id3v2, "USLT")); + } + + AbstractLyrics3 lyrics3Tag = mp3File.getLyrics3Tag(); + if (lyrics3Tag != null) + { + System.out.println("Lyrics3 tag found."); + if (lyrics3Tag instanceof Lyrics3v2) + { + setTagValue(props, PROP_SONG_TITLE, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TIT2")); + setTagValue(props, PROP_ARTIST, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TPE1")); + setTagValue(props, PROP_ALBUM_TITLE, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TALB")); + setTagValue(props, PROP_COMMENT, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "COMM")); + setTagValue(props, PROP_LYRICS, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "SYLT")); + setTagValue(props, PROP_COMPOSER, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TCOM")); + } + } + + } + finally + { + tempFile.delete(); + } + + // Set the destination values + if (props.get(PROP_SONG_TITLE) != null) + { + destination.put(ContentModel.PROP_TITLE, props.get(PROP_SONG_TITLE)); + } + if (props.get(PROP_ARTIST) != null) + { + destination.put(ContentModel.PROP_CREATOR, props.get(PROP_ARTIST)); + } + String description = getDescription(props); + if (description != null) + { + destination.put(ContentModel.PROP_DESCRIPTION, description); + } + } + catch (IOException ioException) + { + // TODO sort out exception handling + throw new RuntimeException("Error reading mp3 file.", ioException); + } + catch (TagException tagException) + { + // TODO sort out exception handling + throw new RuntimeException("Error reading mp3 tag information.", tagException); + } + } + + + /** + * Generate the description + * + * @param props the properties extracted from the file + * @return the description + */ + private String getDescription(Map props) + { + StringBuilder result = new StringBuilder(); + if (props.get(PROP_SONG_TITLE) != null && props.get(PROP_ARTIST) != null && props.get(PROP_ALBUM_TITLE) != null) + { + result + .append(props.get(PROP_SONG_TITLE)) + .append(" - ") + .append(props.get(PROP_ALBUM_TITLE)) + .append(" (") + .append(props.get(PROP_ARTIST)) + .append(")"); + + } + + return result.toString(); + } + + /** + * + * @param props + * @param propQName + * @param propvalue + */ + private void setTagValue(Map props, QName propQName, String propvalue) + { + if (propvalue != null && propvalue.length() != 0) + { + trimPut(propQName, propvalue, props); + } + } + + /** + * + * @param lyrics3Tag + * @param name + * @return + */ + private String getLyrics3v2Value(Lyrics3v2 lyrics3Tag, String name) + { + String result = ""; + Lyrics3v2Field field = lyrics3Tag.getField(name); + if (field != null) + { + AbstractMP3FragmentBody body = field.getBody(); + if (body != null) + { + result = (String)body.getObject("Text"); + } + } + return result; + } + + /** + * Get the ID3V2 tag value in a safe way + * + * @param id3v2 + * @param name + * @return + */ + private String getID3V2Value(AbstractID3v2 id3v2, String name) + { + String result = ""; + + AbstractID3v2Frame frame = id3v2.getFrame(name); + if (frame != null) + { + AbstractMP3FragmentBody body = frame.getBody(); + if (body != null) + { + result = (String)body.getObject("Text"); + } + } + + return result; + } + +} diff --git a/source/java/org/alfresco/repo/content/metadata/MetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/MetadataExtracter.java new file mode 100644 index 0000000000..50b61930da --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/MetadataExtracter.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; + +/** + * + * @author Jesper Steen Møller + */ +public interface MetadataExtracter +{ + /** + * Provides the approximate accuracy with which this extracter can extract + * metadata for the mimetype. + *

    + * + * @param sourceMimetype the source mimetype + * @return Returns a score 0.0 to 1.0. 0.0 indicates that the extraction + * cannot be performed at all. 1.0 indicates that the extraction can + * be performed perfectly. + */ + public double getReliability(String sourceMimetype); + + /** + * Provides an estimate, usually a worst case guess, of how long an + * extraction will take. + *

    + * This method is used to determine, up front, which of a set of equally + * reliant transformers will be used for a specific extraction. + * + * @return Returns the approximate number of milliseconds per transformation + */ + public long getExtractionTime(); + + /** + * Extracts the metadata from the content provided by the reader and source + * mimetype to the supplied map. + *

    + * The extraction viability can be determined by an up front call to + * {@link #getReliability(String)}. + *

    + * The source mimetype must be available on the + * {@link org.alfresco.service.cmr.repository.ContentAccessor#getMimetype()} method + * of the reader. + * + * @param reader the source of the content + * @param destination the destination of the extraction + * @throws ContentIOException if an IO exception occurs + */ + public void extract(ContentReader reader, Map destination) throws ContentIOException; + +} diff --git a/source/java/org/alfresco/repo/content/metadata/MetadataExtracterRegistry.java b/source/java/org/alfresco/repo/content/metadata/MetadataExtracterRegistry.java new file mode 100644 index 0000000000..53940a390a --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/MetadataExtracterRegistry.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; + +/** + * Holds and provides the most appropriate metadate extracter for a particular + * mimetype. + *

    + * The extracters themselves know how well they are able to extract metadata. + * + * @see org.alfresco.repo.content.metadata.MetadataExtracter + * @author Jesper Steen Møller + */ +public class MetadataExtracterRegistry +{ + private static final Log logger = LogFactory.getLog(MetadataExtracterRegistry.class); + + private List extracters; + private Map extracterCache; + + private MimetypeMap mimetypeMap; + /** Controls read access to the cache */ + private Lock extracterCacheReadLock; + /** controls write access to the cache */ + private Lock extracterCacheWriteLock; + + /** + * @param mimetypeMap all the mimetypes available to the system + */ + public MetadataExtracterRegistry(MimetypeMap mimetypeMap) + { + Assert.notNull(mimetypeMap, "The MimetypeMap is mandatory"); + this.mimetypeMap = mimetypeMap; + + extracters = Collections.emptyList(); // just in case it isn't set + extracterCache = new HashMap(17); + + // create lock objects for access to the cache + ReadWriteLock extractionCacheLock = new ReentrantReadWriteLock(); + extracterCacheReadLock = extractionCacheLock.readLock(); + extracterCacheWriteLock = extractionCacheLock.writeLock(); + } + + /** + * Gets the best metadata extracter. This is a combination of the most + * reliable and the most performant extracter. + *

    + * The result is cached for quicker access next time. + * + * @param mimetype the source MIME of the extraction + * @return Returns a metadata extracter that can extract metadata from the + * chosen MIME type. + */ + public MetadataExtracter getExtracter(String sourceMimetype) + { + // check that the mimetypes are valid + if (!mimetypeMap.getMimetypes().contains(sourceMimetype)) + { + throw new AlfrescoRuntimeException("Unknown extraction source mimetype: " + sourceMimetype); + } + + MetadataExtracter extracter = null; + extracterCacheReadLock.lock(); + try + { + if (extracterCache.containsKey(sourceMimetype)) + { + // the translation has been requested before + // it might have been null + return extracterCache.get(sourceMimetype); + } + } + finally + { + extracterCacheReadLock.unlock(); + } + + // the translation has not been requested before + // get a write lock on the cache + // no double check done as it is not an expensive task + extracterCacheWriteLock.lock(); + try + { + // find the most suitable transformer - may be empty list + extracter = findBestExtracter(sourceMimetype); + // store the result even if it is null + extracterCache.put(sourceMimetype, extracter); + return extracter; + } + finally + { + extracterCacheWriteLock.unlock(); + } + } + + /** + * @param sourceMimetype The MIME type under examination + * @return The fastest of the most reliable extracters in + * extracters for the given MIME type. + */ + private MetadataExtracter findBestExtracter(String sourceMimetype) + { + double bestReliability = -1; + long bestTime = Long.MAX_VALUE; + logger.debug("Finding best extracter for " + sourceMimetype); + + MetadataExtracter bestExtracter = null; + + for (MetadataExtracter ext : extracters) + { + double r = ext.getReliability(sourceMimetype); + if (r == bestReliability) + { + long time = ext.getExtractionTime(); + if (time < bestTime) + { + bestExtracter = ext; + bestTime = time; + } + } + else if (r > bestReliability) + { + bestExtracter = ext; + bestReliability = r; + bestTime = ext.getExtractionTime(); + } + } + return bestExtracter; + } + + /** + * Provides a list of self-discovering extracters. + * + * @param transformers all the available extracters that the registry can + * work with + */ + public void setExtracters(List extracters) + { + logger.debug("Setting " + extracters.size() + "new extracters."); + + extracterCacheWriteLock.lock(); + try + { + this.extracters = extracters; + this.extracterCache.clear(); + } + finally + { + extracterCacheWriteLock.unlock(); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracter.java new file mode 100644 index 0000000000..964a4ed698 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracter.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.hpsf.DocumentSummaryInformation; +import org.apache.poi.hpsf.PropertySet; +import org.apache.poi.hpsf.PropertySetFactory; +import org.apache.poi.hpsf.SummaryInformation; +import org.apache.poi.poifs.eventfilesystem.POIFSReader; +import org.apache.poi.poifs.eventfilesystem.POIFSReaderEvent; +import org.apache.poi.poifs.eventfilesystem.POIFSReaderListener; + +/** + * + * @author Jesper Steen Møller + */ +public class OfficeMetadataExtracter extends AbstractMetadataExtracter +{ + + private static final Log logger = LogFactory.getLog(OfficeMetadataExtracter.class); + private static String[] mimeTypes = new String[] { MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_EXCEL, + MimetypeMap.MIMETYPE_PPT }; + + public OfficeMetadataExtracter() + { + super(new HashSet(Arrays.asList(mimeTypes)), 1.0, 1000); + } + + public void extract(ContentReader reader, final Map destination) throws ContentIOException + { + POIFSReaderListener readerListener = new POIFSReaderListener() + { + public void processPOIFSReaderEvent(final POIFSReaderEvent event) + { + try + { + PropertySet ps = PropertySetFactory.create(event.getStream()); + if (ps instanceof SummaryInformation) + { + SummaryInformation si = (SummaryInformation) ps; + // Titled aspect + trimPut(ContentModel.PROP_TITLE, si.getTitle(), destination); + trimPut(ContentModel.PROP_DESCRIPTION, si.getSubject(), destination); + + // Auditable aspect + trimPut(ContentModel.PROP_CREATED, si.getCreateDateTime(), destination); + trimPut(ContentModel.PROP_CREATOR, si.getAuthor(), destination); + trimPut(ContentModel.PROP_MODIFIED, si.getLastSaveDateTime(), destination); + trimPut(ContentModel.PROP_MODIFIER, si.getLastAuthor(), destination); + } + else if (ps instanceof DocumentSummaryInformation) + { + DocumentSummaryInformation dsi = (DocumentSummaryInformation) ps; + + // These are not really interesting to any aspect: + // trimPut(ContentModel.PROP_xxx, dsi.getCompany(), + // destination); + // trimPut(ContentModel.PROP_yyy, dsi.getManager(), + // destination); + } + } + catch (Exception ex) + { + throw new ContentIOException("Property set stream: " + event.getPath() + event.getName(), ex); + } + } + }; + try + { + POIFSReader r = new POIFSReader(); + r.registerListener(readerListener, SummaryInformation.DEFAULT_STREAM_NAME); + r.read(reader.getContentInputStream()); + } + catch (IOException e) + { + throw new ContentIOException("Compound Document SummaryInformation metadata extraction failed: \n" + + " reader: " + reader, + e); + } + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracterTest.java b/source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracterTest.java new file mode 100644 index 0000000000..37f274a7df --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/OfficeMetadataExtracterTest.java @@ -0,0 +1,60 @@ +package org.alfresco.repo.content.metadata; + +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.OfficeMetadataExtracter + * @author Jesper Steen Møller + */ +public class OfficeMetadataExtracterTest extends AbstractMetadataExtracterTest +{ + private static final Log logger = LogFactory.getLog(OfficeMetadataExtracterTest.class); + private MetadataExtracter extracter; + + public void onSetUpInTransaction() throws Exception + { + extracter = new OfficeMetadataExtracter(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected MetadataExtracter getExtracter() + { + return extracter; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype text should not be supported", 0.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_WORD); + assertEquals("Word should be supported", 1.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_EXCEL); + assertEquals("Excel should be supported", 1.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_PPT); + assertEquals("PowerPoint should be supported", 1.0, reliability); + } + + public void testWordExtraction() throws Exception + { + testCommonMetadata(extractFromExtension("doc", MimetypeMap.MIMETYPE_WORD)); + } + + public void testExcelExtraction() throws Exception + { + testCommonMetadata(extractFromExtension("xls", MimetypeMap.MIMETYPE_EXCEL)); + } + + public void testPowerPointExtraction() throws Exception + { + testCommonMetadata(extractFromExtension("ppt", MimetypeMap.MIMETYPE_PPT)); + } + +} diff --git a/source/java/org/alfresco/repo/content/metadata/OpenDocumentMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/OpenDocumentMetadataExtracter.java new file mode 100644 index 0000000000..8bdc6c96a6 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/OpenDocumentMetadataExtracter.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2005 Antti Jokipii + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.catcode.odf.ODFMetaFileAnalyzer; +import com.catcode.odf.OpenDocumentMetadata; + +/** + * Metadata extractor for the + * {@link org.alfresco.repo.content.MimetypeMap#MIMETYPE_OPENDOCUMENT_TEXT MIMETYPE_OPENDOCUMENT_XXX} + * mimetypes. + * + * @author Antti Jokipii + */ +public class OpenDocumentMetadataExtracter extends AbstractMetadataExtracter +{ + private static final Log logger = LogFactory.getLog(OpenDocumentMetadataExtracter.class); + + private static String[] mimeTypes = new String[] { + MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT, + MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_GRAPHICS, + MimetypeMap.MIMETYPE_OPENDOCUMENT_GRAPHICS_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_PRESENTATION, + MimetypeMap.MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_SPREADSHEET, + MimetypeMap.MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_CHART, + MimetypeMap.MIMETYPE_OPENDOCUMENT_CHART_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_IMAGE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_IMAGE_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_FORMULA, + MimetypeMap.MIMETYPE_OPENDOCUMENT_FORMULA_TEMPLATE, + MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT_MASTER, + MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT_WEB, + MimetypeMap.MIMETYPE_OPENDOCUMENT_DATABASE, }; + + public OpenDocumentMetadataExtracter() + { + super(new HashSet(Arrays.asList(mimeTypes)), 1.00, 1000); + } + + public void extract(ContentReader reader, Map destination) throws ContentIOException + { + ODFMetaFileAnalyzer analyzer = new ODFMetaFileAnalyzer(); + try + { + // stream the document in + OpenDocumentMetadata docInfo = analyzer.analyzeZip(reader.getContentInputStream()); + + if (docInfo != null) + { + // set the metadata + destination.put(ContentModel.PROP_CREATOR, docInfo.getCreator()); + destination.put(ContentModel.PROP_TITLE, docInfo.getTitle()); + destination.put(ContentModel.PROP_DESCRIPTION, docInfo.getDescription()); + destination.put(ContentModel.PROP_CREATED, docInfo.getCreationDate()); + } + } + catch (Throwable e) + { + String message = "Metadata extraction failed: \n" + + " reader: " + reader; + logger.debug(message, e); + throw new ContentIOException(message, e); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java new file mode 100644 index 0000000000..65724a1420 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Calendar; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.pdfbox.pdmodel.PDDocument; +import org.pdfbox.pdmodel.PDDocumentInformation; + +/** + * + * @author Jesper Steen Møller + */ +public class PdfBoxMetadataExtracter extends AbstractMetadataExtracter +{ + + private static final Log logger = LogFactory.getLog(PdfBoxMetadataExtracter.class); + + public PdfBoxMetadataExtracter() + { + super(MimetypeMap.MIMETYPE_PDF, 1.0, 1000); + } + + public void extract(ContentReader reader, Map destination) throws ContentIOException + { + if (!MimetypeMap.MIMETYPE_PDF.equals(reader.getMimetype())) + { + logger.debug("No metadata extracted for " + reader.getMimetype()); + return; + } + PDDocument pdf = null; + try + { + // stream the document in + pdf = PDDocument.load(reader.getContentInputStream()); + // Scoop out the metadata + PDDocumentInformation docInfo = pdf.getDocumentInformation(); + + trimPut(ContentModel.PROP_CREATOR, docInfo.getAuthor(), destination); + trimPut(ContentModel.PROP_TITLE, docInfo.getTitle(), destination); + trimPut(ContentModel.PROP_DESCRIPTION, docInfo.getSubject(), destination); + + Calendar created = docInfo.getCreationDate(); + if (created != null) + destination.put(ContentModel.PROP_CREATED, created.getTime()); + } + catch (IOException e) + { + throw new ContentIOException("PDF metadata extraction failed: \n" + + " reader: " + reader); + } + finally + { + if (pdf != null) + { + try + { + pdf.close(); + } + catch (Throwable e) + { + e.printStackTrace(); + } + } + } + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracterTest.java b/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracterTest.java new file mode 100644 index 0000000000..f218508d22 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracterTest.java @@ -0,0 +1,43 @@ +package org.alfresco.repo.content.metadata; + +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.PdfBoxContentTransformer + * @author Jesper Steen Møller + */ +public class PdfBoxMetadataExtracterTest extends AbstractMetadataExtracterTest +{ + private static final Log logger = LogFactory.getLog(PdfBoxMetadataExtracterTest.class); + private MetadataExtracter extracter; + + public void onSetUpInTransaction() throws Exception + { + extracter = new PdfBoxMetadataExtracter(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected MetadataExtracter getExtracter() + { + return extracter; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should not be supported", 0.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_PDF); + assertEquals("Mimetype should be supported", 1.0, reliability); + } + + public void testPdfExtraction() throws Exception + { + testCommonMetadata(extractFromExtension("pdf", MimetypeMap.MIMETYPE_PDF)); + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/StringMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/StringMetadataExtracter.java new file mode 100644 index 0000000000..29cba14764 --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/StringMetadataExtracter.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * @author Jesper Steen Møller + */ +public class StringMetadataExtracter implements MetadataExtracter +{ + public static final String PREFIX_TEXT = "text/"; + + private static final Log logger = LogFactory.getLog(StringMetadataExtracter.class); + + public double getReliability(String sourceMimetype) + { + if (sourceMimetype.startsWith(PREFIX_TEXT)) + return 0.1; + else + return 0.0; + } + + public long getExtractionTime() + { + return 1000; + } + + public void extract(ContentReader reader, Map destination) throws ContentIOException + { + if (logger.isDebugEnabled()) + { + logger.debug("No metadata extracted for " + reader.getMimetype()); + } + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracter.java new file mode 100644 index 0000000000..68785e1feb --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracter.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.Serializable; +import java.net.ConnectException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; + +import net.sf.joott.uno.UnoConnection; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.sun.star.beans.PropertyValue; +import com.sun.star.beans.XPropertySet; +import com.sun.star.document.XDocumentInfoSupplier; +import com.sun.star.frame.XComponentLoader; +import com.sun.star.lang.XComponent; +import com.sun.star.ucb.XFileIdentifierConverter; +import com.sun.star.uno.UnoRuntime; + +/** + * + * @author Jesper Steen Møller + */ +public class UnoMetadataExtracter extends AbstractMetadataExtracter +{ + + private static final Log logger = LogFactory.getLog(UnoMetadataExtracter.class); + + private static String[] mimeTypes = new String[] { + MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT, + MimetypeMap.MIMETYPE_OPENOFFICE_WRITER, + // Add the other OpenOffice.org stuff here + // In fact, other types may apply as well, but should be counted as lower + // quality since they involve conversion. + }; + + public UnoMetadataExtracter(MimetypeMap mimetypeMap, String connectionUrl) + { + super(new HashSet(Arrays.asList(mimeTypes)), 1.00, 10000); + this.mimetypeMap = mimetypeMap; + init(connectionUrl); + } + + public UnoMetadataExtracter(MimetypeMap mimetypeMap) + { + this(mimetypeMap, UnoConnection.DEFAULT_CONNECTION_STRING); + } + + private MimetypeMap mimetypeMap; + private MyUnoConnection connection; + private boolean isConnected; + + /** + * @param unoConnectionUrl the URL of the Uno server + */ + private synchronized void init(String unoConnectionUrl) + { + connection = new MyUnoConnection(unoConnectionUrl); + // attempt to make an connection + try + { + connection.connect(); + isConnected = true; + } + catch (ConnectException e) + { + isConnected = false; + } + } + + /** + * @return Returns true if a connection to the Uno server could be + * established + */ + public boolean isConnected() + { + return isConnected; + } + + public void extract(ContentReader reader, final Map destination) throws ContentIOException + { + String sourceMimetype = reader.getMimetype(); + + // create temporary files to convert from and to + File tempFromFile = TempFileProvider.createTempFile("UnoContentTransformer", "." + + mimetypeMap.getExtension(sourceMimetype)); + // download the content from the source reader + reader.getContent(tempFromFile); + String sourceUrl = tempFromFile.toString(); + try + { + sourceUrl = toUrl(tempFromFile, connection); + + // UNO Interprocess Bridge *should* be thread-safe, but... + synchronized (connection) + { + XComponentLoader desktop = connection.getDesktop(); + XComponent document = desktop.loadComponentFromURL( + sourceUrl, + "_blank", + 0, + new PropertyValue[] { property("Hidden", Boolean.TRUE) }); + if (document == null) + { + throw new FileNotFoundException("could not open source document: " + sourceUrl); + } + try + { + XDocumentInfoSupplier infoSupplier = (XDocumentInfoSupplier) UnoRuntime.queryInterface( + XDocumentInfoSupplier.class, document); + XPropertySet propSet = (XPropertySet) UnoRuntime.queryInterface( + XPropertySet.class, + infoSupplier + .getDocumentInfo()); + + // Titled aspect + trimPut(ContentModel.PROP_TITLE, propSet.getPropertyValue("Title"), destination); + trimPut(ContentModel.PROP_DESCRIPTION, propSet.getPropertyValue("Subject"), destination); + + // Auditable aspect + // trimPut(ContentModel.PROP_CREATED, + // si.getCreateDateTime(), destination); + trimPut(ContentModel.PROP_CREATOR, propSet.getPropertyValue("Author"), destination); + // trimPut(ContentModel.PROP_MODIFIED, + // si.getLastSaveDateTime(), destination); + // trimPut(ContentModel.PROP_MODIFIER, si.getLastAuthor(), + // destination); + } + finally + { + document.dispose(); + } + } + } + catch (Throwable e) + { + throw new ContentIOException("Conversion failed: \n" + + " source: " + sourceUrl + "\n", + e); + } + } + + public String toUrl(File file, MyUnoConnection connection) throws ConnectException + { + Object fcp = connection.getFileContentService(); + XFileIdentifierConverter fic = (XFileIdentifierConverter) UnoRuntime.queryInterface( + XFileIdentifierConverter.class, fcp); + return fic.getFileURLFromSystemPath("", file.getAbsolutePath()); + } + + public double getReliability(String sourceMimetype) + { + if (isConnected()) + return super.getReliability(sourceMimetype); + else + return 0.0; + } + + private static PropertyValue property(String name, Object value) + { + PropertyValue property = new PropertyValue(); + property.Name = name; + property.Value = value; + return property; + } + + static class MyUnoConnection extends UnoConnection + { + public MyUnoConnection(String url) + { + super(url); + } + + public Object getFileContentService() throws ConnectException + { + return getService("com.sun.star.ucb.FileContentProvider"); + } + } +} diff --git a/source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracterTest.java b/source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracterTest.java new file mode 100644 index 0000000000..b6f9d5f67b --- /dev/null +++ b/source/java/org/alfresco/repo/content/metadata/UnoMetadataExtracterTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Jesper Steen Møller + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.metadata; + +import org.alfresco.repo.content.MimetypeMap; + +/** + * @see org.alfresco.repo.content.transform.UnoMetadataExtracter + * @author Jesper Steen Møller + */ +public class UnoMetadataExtracterTest extends AbstractMetadataExtracterTest +{ + private UnoMetadataExtracter extracter; + + public void onSetUpInTransaction() throws Exception + { + extracter = new UnoMetadataExtracter(mimetypeMap); + } + + /** + * @return Returns the same extracter regardless - it is allowed + */ + protected MetadataExtracter getExtracter() + { + return extracter; + } + + public void testReliability() throws Exception + { + if (!extracter.isConnected()) + { + return; + } + + double reliability = 0.0; + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype text should not be supported", 0.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT); + assertEquals("OpenOffice 2.0 Writer (OpenDoc) should be supported", 1.0, reliability); + + reliability = extracter.getReliability(MimetypeMap.MIMETYPE_OPENOFFICE_WRITER); + assertEquals("OpenOffice 1.0 Writer should be supported", 1.0, reliability); + } + + public void testOOo20WriterExtraction() throws Exception + { + if (!extracter.isConnected()) + { + return; + } + + testCommonMetadata(extractFromExtension("odt", MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT)); + } + + public void testOOo10WriterExtraction() throws Exception + { + if (!extracter.isConnected()) + { + return; + } + + testCommonMetadata(extractFromExtension("sxw", MimetypeMap.MIMETYPE_OPENOFFICE_WRITER)); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/AbstractContentTransformer.java b/source/java/org/alfresco/repo/content/transform/AbstractContentTransformer.java new file mode 100644 index 0000000000..b5f8cd3c2d --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/AbstractContentTransformer.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.util.Collections; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentAccessor; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides basic services for {@link org.alfresco.repo.content.transform.ContentTransformer} + * implementations. + *

    + * This class maintains the performance measures for the transformers as well, making sure that + * there is an extra penalty for transformers that fail regularly. + * + * @author Derek Hulley + */ +public abstract class AbstractContentTransformer implements ContentTransformer +{ + private static final Log logger = LogFactory.getLog(AbstractContentTransformer.class); + + private MimetypeService mimetypeService; + private double averageTime = 0.0; + private long count = 0L; + + /** + * All transformers start with an average transformation time of 0.0ms. + */ + protected AbstractContentTransformer() + { + averageTime = 0.0; + } + + /** + * Helper setter of the mimetype service. This is not always required. + * + * @param mimetypeService + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + /** + * @return Returns the mimetype helper + */ + protected MimetypeService getMimetypeService() + { + return mimetypeService; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()) + .append("[ average=").append((long)averageTime).append("ms") + .append("]"); + return sb.toString(); + } + + /** + * Convenience to fetch and check the mimetype for the given content + * + * @param content the reader/writer for the content + * @return Returns the mimetype for the content + * @throws AlfrescoRuntimeException if the content doesn't have a mimetype + */ + protected String getMimetype(ContentAccessor content) + { + String mimetype = content.getMimetype(); + if (mimetype == null) + { + throw new AlfrescoRuntimeException("Mimetype is mandatory for transformation: " + content); + } + // done + return mimetype; + } + + /** + * Convenience method to check the reliability of a transformation + * + * @param reader + * @param writer + * @throws AlfrescoRuntimeException if the reliability isn't > 0 + */ + protected void checkReliability(ContentReader reader, ContentWriter writer) + { + String sourceMimetype = getMimetype(reader); + String targetMimetype = getMimetype(writer); + if (getReliability(sourceMimetype, targetMimetype) <= 0.0) + { + throw new AlfrescoRuntimeException("Zero scoring transformation attempted: \n" + + " reader: " + reader + "\n" + + " writer: " + writer); + } + // it all checks out OK + } + + /** + * Method to be implemented by subclasses wishing to make use of the common infrastructural code + * provided by this class. + * + * @param reader the source of the content to transform + * @param writer the target to which to write the transformed content + * @param options a map of options to use when performing the transformation. The map + * will never be null. + * @throws Exception exceptions will be handled by this class - subclasses can throw anything + */ + protected abstract void transformInternal( + ContentReader reader, + ContentWriter writer, + Map options) throws Exception; + + /** + * @see #transform(ContentReader, ContentWriter, Map) + * @see #transformInternal(ContentReader, ContentWriter, Map) + */ + public final void transform(ContentReader reader, ContentWriter writer) throws ContentIOException + { + transform(reader, writer, null); + } + + /** + * Performs the following: + *

      + *
    • Times the transformation
    • + *
    • Ensures that the transformation is allowed
    • + *
    • Calls the subclass implementation of {@link #transformInternal(ContentReader, ContentWriter)}
    • + *
    • Transforms any exceptions generated
    • + *
    • Logs a successful transformation
    • + *
    + * Subclass need only be concerned with performing the transformation. + *

    + * If the options provided are null, then an empty map will be created. + */ + public final void transform( + ContentReader reader, + ContentWriter writer, + Map options) throws ContentIOException + { + // begin timing + long before = System.currentTimeMillis(); + + // check the reliability + checkReliability(reader, writer); + + // check options map + if (options == null) + { + options = Collections.emptyMap(); + } + + try + { + transformInternal(reader, writer, options); + } + catch (Throwable e) + { + // Make sure that this transformation gets set back i.t.o. time taken. + // This will ensure that transformers that compete for the same transformation + // will be prejudiced against transformers that tend to fail + recordTime(10000); // 10 seconds, i.e. rubbish + + throw new ContentIOException("Content conversion failed: \n" + + " reader: " + reader + "\n" + + " writer: " + writer + "\n" + + " options: " + options, + e); + } + + // record time + long after = System.currentTimeMillis(); + recordTime(after - before); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Completed transformation: \n" + + " reader: " + reader + "\n" + + " writer: " + writer + "\n" + + " options: " + options + "\n" + + " transformer: " + this); + } + } + + /** + * @return Returns the calculated running average of the current transformations + */ + public synchronized long getTransformationTime() + { + return (long) averageTime; + } + + /** + * Records and updates the average transformation time for this transformer. + *

    + * Subclasses should call this after every transformation in order to keep + * the running average of the transformation times up to date. + *

    + * This method is thread-safe. The time spent in this method is negligible + * so the impact will be minor. + * + * @param transformationTime the time it took to perform the transformation. + * The value may be 0. + */ + protected final synchronized void recordTime(long transformationTime) + { + if (count == Long.MAX_VALUE) + { + // we have reached the max count - reduce it by half + // the average fluctuation won't be extreme + count /= 2L; + } + // adjust the average + count++; + double diffTime = ((double) transformationTime) - averageTime; + averageTime += diffTime / (double) count; + } +} diff --git a/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerTest.java new file mode 100644 index 0000000000..504f45a71f --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.TempFileProvider; + +/** + * Provides a base set of tests for testing + * {@link org.alfresco.repo.content.transform.ContentTransformer} + * implementations. + * + * @author Derek Hulley + */ +public abstract class AbstractContentTransformerTest extends BaseSpringTest +{ + private static String QUICK_CONTENT = "The quick brown fox jumps over the lazy dog"; + private static String[] QUICK_WORDS = new String[] { + "quick", "brown", "fox", "jumps", "lazy", "dog"}; + + protected MimetypeMap mimetypeMap; + protected ContentTransformer transformer; + + public final void setMimetypeMap(MimetypeMap mimetypeMap) + { + this.mimetypeMap = mimetypeMap; + } + + /** + * Fetches a transformer to test for a given transformation. The transformer + * does not have to be reliable for the given format - if it isn't + * then it will be ignored. + * + * @param sourceMimetype the sourceMimetype to be tested + * @param targetMimetype the targetMimetype to be tested + * @return Returns the ContentTranslators that will be tested by + * the methods implemented in this class. A null return value is + * acceptable if the source and target mimetypes are not of interest. + */ + protected abstract ContentTransformer getTransformer(String sourceMimetype, String targetMimetype); + + /** + * Ensures that the temp locations are cleaned out before the tests start + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + // perform a little cleaning up + long now = System.currentTimeMillis(); + TempFileProvider.TempFileCleanerJob.removeFiles(now); + } + + /** + * Check that all objects are present + */ + public void testSetUp() throws Exception + { + assertNotNull("MimetypeMap not present", mimetypeMap); + // check that the quick resources are available + File sourceFile = AbstractContentTransformerTest.loadQuickTestFile("txt"); + assertNotNull(sourceFile); + } + + /** + * Helper method to load one of the "The quick brown fox" files from the + * classpath. + * + * @param extension the extension of the file required + * @return Returns a test resource loaded from the classpath or null if + * no resource could be found. + * @throws IOException + */ + public static File loadQuickTestFile(String extension) throws IOException + { + URL url = AbstractContentTransformerTest.class.getClassLoader().getResource("quick/quick." + extension); + if (url == null) + { + return null; + } + File file = new File(url.getFile()); + if (!file.exists()) + { + return null; + } + return file; + } + + /** + * Tests the full range of transformations available on the + * {@link #getTransformer(String, String) transformer} subject to the + * {@link org.alfresco.util.test.QuickFileTest available test files} + * and the {@link ContentTransformer#getReliability(String, String) reliability} of + * the {@link #getTransformer(String, String) transformer} itself. + *

    + * Each transformation is repeated several times, with a transformer being + * {@link #getTransformer(String, String) requested} for each transformation. In the + * case where optimizations are being done around the selection of the most + * appropriate transformer, different transformers could be used during the iteration + * process. + */ + public void testAllConversions() throws Exception + { + // get all mimetypes + List mimetypes = mimetypeMap.getMimetypes(); + for (String sourceMimetype : mimetypes) + { + // attempt to get a source file for each mimetype + String sourceExtension = mimetypeMap.getExtension(sourceMimetype); + File sourceFile = AbstractContentTransformerTest.loadQuickTestFile(sourceExtension); + if (sourceFile == null) + { + continue; // no test file available for that extension + } + + // attempt to convert to every other mimetype + for (String targetMimetype : mimetypes) + { + ContentWriter targetWriter = null; + // construct a reader onto the source file + ContentReader sourceReader = new FileContentReader(sourceFile); + + // perform the transformation several times so that we get a good idea of performance + int count = 0; + for (int i = 0; i < 5; i++) + { + // must we test the transformation? + ContentTransformer transformer = getTransformer(sourceMimetype, targetMimetype); + if (transformer == null) + { + break; // test is not required + } + else if (transformer.getReliability(sourceMimetype, targetMimetype) <= 0.0) + { + break; // not reliable for this transformation + } + + // make a writer for the target file + String targetExtension = mimetypeMap.getExtension(targetMimetype); + File targetFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_" + getName() + "_" + sourceExtension + "_", + "." + targetExtension); + targetWriter = new FileContentWriter(targetFile); + + // do the transformation + sourceReader.setMimetype(sourceMimetype); + targetWriter.setMimetype(targetMimetype); + transformer.transform(sourceReader.getReader(), targetWriter); + + // if the target format is any type of text, then it must contain the 'quick' phrase + if (targetMimetype.equals(MimetypeMap.MIMETYPE_TEXT_PLAIN)) + { + ContentReader targetReader = targetWriter.getReader(); + String checkContent = targetReader.getContentString(); + assertTrue("Quick phrase not present in document converted to text: \n" + + " transformer: " + transformer + "\n" + + " source: " + sourceReader + "\n" + + " target: " + targetWriter, + checkContent.contains(QUICK_CONTENT)); + } + else if (targetMimetype.startsWith(StringExtractingContentTransformer.PREFIX_TEXT)) + { + ContentReader targetReader = targetWriter.getReader(); + String checkContent = targetReader.getContentString(); + // essentially check that FTS indexing can use the conversion properly + for (int word = 0; word < QUICK_WORDS.length; word++) + { + assertTrue("Quick phrase word not present in document converted to text: \n" + + " transformer: " + transformer + "\n" + + " source: " + sourceReader + "\n" + + " target: " + targetWriter + "\n" + + " word: " + word, + checkContent.contains(QUICK_WORDS[word])); + } + } + // increment count + count++; + } + + if (logger.isDebugEnabled()) + { + logger.debug("Transformation performed " + count + " time: " + + sourceMimetype + " --> " + targetMimetype + "\n" + + " source: " + sourceReader + "\n" + + " target: " + targetWriter + "\n" + + " transformer: " + transformer); + } + } + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformer.java b/source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformer.java new file mode 100644 index 0000000000..8245c94940 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.util.Map; + +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Allows direct streaming from source to target when the respective mimetypes + * are identical, except where the mimetype is text. + *

    + * Text has to be transformed based on the encoding even if the mimetypes don't + * reflect it. + * + * @see org.alfresco.repo.content.transform.StringExtractingContentTransformer + * + * @author Derek Hulley + */ +public class BinaryPassThroughContentTransformer extends AbstractContentTransformer +{ + private static final Log logger = LogFactory.getLog(BinaryPassThroughContentTransformer.class); + + /** + * @return Returns 1.0 if the formats are identical and not text + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (sourceMimetype.startsWith(StringExtractingContentTransformer.PREFIX_TEXT)) + { + // we can only stream binary content through + return 0.0; + } + else if (!sourceMimetype.equals(targetMimetype)) + { + // no transformation is possible so formats must be exact + return 0.0; + } + else + { + // formats are the same and are not text + return 1.0; + } + } + + /** + * Performs a direct stream provided the preconditions are met + */ + public void transformInternal( + ContentReader reader, + ContentWriter writer, + Map options) throws Exception + { + // just stream it + writer.putContent(reader.getContentInputStream()); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformerTest.java new file mode 100644 index 0000000000..772de4dafd --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/BinaryPassThroughContentTransformerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.BinaryPassThroughContentTransformer + * + * @author Derek Hulley + */ +public class BinaryPassThroughContentTransformerTest extends AbstractContentTransformerTest +{ + private static final Log logger = LogFactory.getLog(BinaryPassThroughContentTransformerTest.class); + + private ContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + transformer = new BinaryPassThroughContentTransformer(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_XML, MimetypeMap.MIMETYPE_XML); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_WORD); + assertEquals("Mimetype should be supported", 1.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_EXCEL); + assertEquals("Mimetype should be supported", 1.0, reliability); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java b/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java new file mode 100644 index 0000000000..dded8bbfca --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java @@ -0,0 +1,149 @@ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; +import org.springframework.beans.factory.InitializingBean; + +/** + * Transformer that passes a document through several nested transformations + * in order to accomplish its goal. + * + * @author Derek Hulley + */ +public class ComplexContentTransformer extends AbstractContentTransformer implements InitializingBean +{ + private List transformers; + private List intermediateMimetypes; + + public ComplexContentTransformer() + { + } + + /** + * The list of transformers to use. + *

    + * If a single transformer is supplied, then it will still be used. + * + * @param transformers list of at least one transformer + */ + public void setTransformers(List transformers) + { + this.transformers = transformers; + } + + /** + * Set the intermediate mimetypes that the transformer must take the content + * through. If the transformation A..B..C is performed in order to + * simulate A..C, then B is the intermediate mimetype. There + * must always be n-1 intermediate mimetypes, where n is the + * number of {@link #setTransformers(List) transformers} taking part in the + * transformation. + * + * @param intermediateMimetypes intermediate mimetypes to transition the content + * through. + */ + public void setIntermediateMimetypes(List intermediateMimetypes) + { + this.intermediateMimetypes = intermediateMimetypes; + } + + /** + * Ensures that required properties have been set + */ + public void afterPropertiesSet() throws Exception + { + if (transformers == null || transformers.size() == 0) + { + throw new AlfrescoRuntimeException("At least one inner transformer must be supplied: " + this); + } + if (intermediateMimetypes == null || intermediateMimetypes.size() != transformers.size() - 1) + { + throw new AlfrescoRuntimeException( + "There must be n-1 intermediate mimetypes, where n is the number of transformers"); + } + if (getMimetypeService() == null) + { + throw new AlfrescoRuntimeException("'mimetypeService' is a required property"); + } + } + + /** + * @return Returns the multiple of the reliabilities of the chain of transformers + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + double reliability = 1.0; + String currentSourceMimetype = sourceMimetype; + + Iterator transformerIterator = transformers.iterator(); + Iterator intermediateMimetypeIterator = intermediateMimetypes.iterator(); + while (transformerIterator.hasNext()) + { + ContentTransformer transformer = transformerIterator.next(); + // determine the target mimetype. This is the final target if we are on the last transformation + String currentTargetMimetype = null; + if (!transformerIterator.hasNext()) + { + currentTargetMimetype = targetMimetype; + } + else + { + // use an intermediate transformation mimetype + currentTargetMimetype = intermediateMimetypeIterator.next(); + } + // the reliability is a multiple + reliability *= transformer.getReliability(currentSourceMimetype, currentTargetMimetype); + // move the source on + currentSourceMimetype = currentTargetMimetype; + } + // done + return reliability; + } + + @Override + public void transformInternal( + ContentReader reader, + ContentWriter writer, + Map options) throws Exception + { + ContentReader currentReader = reader; + + Iterator transformerIterator = transformers.iterator(); + Iterator intermediateMimetypeIterator = intermediateMimetypes.iterator(); + while (transformerIterator.hasNext()) + { + ContentTransformer transformer = transformerIterator.next(); + // determine the target mimetype. This is the final target if we are on the last transformation + ContentWriter currentWriter = null; + if (!transformerIterator.hasNext()) + { + currentWriter = writer; + } + else + { + String nextMimetype = intermediateMimetypeIterator.next(); + // make a temp file writer with the correct extension + String sourceExt = getMimetypeService().getExtension(currentReader.getMimetype()); + String targetExt = getMimetypeService().getExtension(nextMimetype); + File tempFile = TempFileProvider.createTempFile( + "ComplextTransformer_intermediate_" + sourceExt + "_", + "." + targetExt); + currentWriter = new FileContentWriter(tempFile); + currentWriter.setMimetype(nextMimetype); + } + // transform + transformer.transform(currentReader, currentWriter, options); + // move the source on + currentReader = currentWriter.getReader(); + } + // done + } +} diff --git a/source/java/org/alfresco/repo/content/transform/ComplexContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/ComplexContentTransformerTest.java new file mode 100644 index 0000000000..43124cd9a3 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/ComplexContentTransformerTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.alfresco.repo.content.MimetypeMap; + +/** + * Tests a transformation from Powerpoint->PDF->Text. + * + * @see org.alfresco.repo.content.transform.ComplexContentTransformer + * + * @author Derek Hulley + */ +public class ComplexContentTransformerTest extends AbstractContentTransformerTest +{ + private ComplexContentTransformer transformer; + private boolean isAvailable; + + public void onSetUpInTransaction() throws Exception + { + ContentTransformer unoTransformer = (ContentTransformer) applicationContext.getBean("transformer.OpenOffice"); + ContentTransformer pdfBoxTransformer = (ContentTransformer) applicationContext.getBean("transformer.PdfBox"); + // make sure that they are working for this test + if (unoTransformer.getReliability(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_PDF) == 0.0) + { + isAvailable = false; + return; + } + else if (pdfBoxTransformer.getReliability(MimetypeMap.MIMETYPE_PDF, MimetypeMap.MIMETYPE_TEXT_PLAIN) == 0.0) + { + isAvailable = false; + return; + } + else + { + isAvailable = true; + } + + transformer = new ComplexContentTransformer(); + transformer.setMimetypeService(mimetypeMap); + // set the transformer list + List transformers = new ArrayList(2); + transformers.add(unoTransformer); + transformers.add(pdfBoxTransformer); + transformer.setTransformers(transformers); + // set the intermediate mimetypes + List intermediateMimetypes = Collections.singletonList(MimetypeMap.MIMETYPE_PDF); + transformer.setIntermediateMimetypes(intermediateMimetypes); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + if (!isAvailable) + { + return; + } + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_PDF); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should be supported", 1.0, reliability); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/CompoundContentTransformer.java b/source/java/org/alfresco/repo/content/transform/CompoundContentTransformer.java new file mode 100644 index 0000000000..e927ff4aaa --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/CompoundContentTransformer.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.util.LinkedList; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A chain of transformations that is able to produce non-zero reliability + * transformation from one mimetype to another. + *

    + * The reliability of the chain is the product of all the individual + * transformations. + * + * @author Derek Hulley + */ +public class CompoundContentTransformer implements ContentTransformer +{ + private static final Log logger = LogFactory.getLog(CompoundContentTransformer.class); + + /** a sequence of transformers to apply */ + private LinkedList chain; + /** the combined reliability of all the transformations in the chain */ + private double reliability; + + public CompoundContentTransformer() + { + chain = new LinkedList(); + reliability = 1.0; + } + + /** + * Adds a transformation to the chain. The reliability of each transformation + * added must be greater than 0.0. + * + * @param sourceMimetype + * @param targetMimetype + * @param transformer the transformer that will transform from the source to + * the target mimetype + */ + public void addTransformation(String sourceMimetype, String targetMimetype, ContentTransformer transformer) + { + // create a transformation that aggregates the transform info + Transformation transformation = new Transformation( + transformer, + sourceMimetype, + targetMimetype); + // add to the chain + chain.add(transformation); + // recalculate combined reliability + double transformerReliability = transformer.getReliability(sourceMimetype, targetMimetype); + if (transformerReliability <= 0.0 || transformerReliability > 1.0) + { + throw new AlfrescoRuntimeException( + "Reliability of transformer must be between 0.0 and 1.0: \n" + + " transformer: " + transformer + "\n" + + " source: " + sourceMimetype + "\n" + + " target: " + targetMimetype + "\n" + + " reliability: " + transformerReliability); + } + this.reliability *= transformerReliability; + } + + /** + * In order to score anything, the source mimetype must match the source + * mimetype of the first transformer and the target mimetype must match + * the target mimetype of the last transformer in the chain. + * + * @return Returns the product of the individual reliability scores of the + * transformations in the chain + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (chain.size() == 0) + { + // no transformers therefore no transformation possible + return 0.0; + } + Transformation first = chain.getFirst(); + Transformation last = chain.getLast(); + if (!first.getSourceMimetype().equals(sourceMimetype) + && last.getTargetMimetype().equals(targetMimetype)) + { + // the source type of the first transformation must match the source + // the target type of the last transformation must match the target + return 0.0; + } + return reliability; + } + + /** + * @return Returns 0 if there are no transformers in the chain otherwise + * returns the sum of all the individual transformation times + */ + public long getTransformationTime() + { + long transformationTime = 0L; + for (Transformation transformation : chain) + { + ContentTransformer transformer = transformation.transformer; + transformationTime += transformer.getTransformationTime(); + } + return transformationTime; + } + + /** + * + */ + public void transform(ContentReader reader, ContentWriter writer) throws ContentIOException + { + transform(reader, writer, null); + } + + /** + * Executes each transformer in the chain, passing the content between them + */ + public void transform(ContentReader reader, ContentWriter writer, Map options) + throws ContentIOException + { + if (chain.size() == 0) + { + throw new AlfrescoRuntimeException("No transformations present in chain"); + } + + // check that the mimetypes of the transformation are valid for the chain + String sourceMimetype = reader.getMimetype(); + String targetMimetype = writer.getMimetype(); + Transformation firstTransformation = chain.getFirst(); + Transformation lastTransformation = chain.getLast(); + if (!firstTransformation.getSourceMimetype().equals(sourceMimetype) + && lastTransformation.getTargetMimetype().equals(targetMimetype)) + { + throw new AlfrescoRuntimeException("Attempting to perform unreliable transformation: \n" + + " reader: " + reader + "\n" + + " writer: " + writer); + } + + ContentReader currentReader = reader; + ContentWriter currentWriter = null; + int currentIndex = 0; + for (Transformation transformation : chain) + { + boolean last = (currentIndex == chain.size() - 1); + if (last) + { + // we are on the last transformation so use the final output writer + currentWriter = writer; + } + else + { + // have to create an intermediate writer - just use a file writer + File tempFile = TempFileProvider.createTempFile("transform", ".tmp"); + currentWriter = new FileContentWriter(tempFile); + // set the writer's mimetype to conform to the transformation we are using + currentWriter.setMimetype(transformation.getTargetMimetype()); + } + // transform from the current reader to the current writer + transformation.execute(currentReader, currentWriter, options); + if (!currentWriter.isClosed()) + { + throw new AlfrescoRuntimeException("Writer not closed by transformation: \n" + + " transformation: " + transformation + "\n" + + " writer: " + currentWriter); + } + // if we have more transformations, then use the written content + // as the next source + if (!last) + { + currentReader = currentWriter.getReader(); + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Executed complex transformation: \n" + + " chain: " + chain + "\n" + + " reader: " + reader + "\n" + + " writer: " + writer); + } + } + + /** + * A transformation that contains the transformer as well as the + * transformation mimetypes to be used + */ + public static class Transformation extends ContentTransformerRegistry.TransformationKey + { + private ContentTransformer transformer; + public Transformation(ContentTransformer transformer, String sourceMimetype, String targetMimetype) + { + super(sourceMimetype, targetMimetype); + this.transformer = transformer; + } + + /** + * Executs the transformation + * + * @param reader the reader from which to read the content + * @param writer the writer to write content to + * @param options the options to execute with + * @throws ContentIOException if the transformation fails + */ + public void execute(ContentReader reader, ContentWriter writer, Map options) + throws ContentIOException + { + String sourceMimetype = getSourceMimetype(); + String targetMimetype = getTargetMimetype(); + // check that the source and target mimetypes of the reader and writer match + if (!sourceMimetype.equals(reader.getMimetype())) + { + throw new AlfrescoRuntimeException("The source mimetype doesn't match the reader's mimetype: \n" + + " source mimetype: " + sourceMimetype + "\n" + + " reader: " + reader); + } + if (!targetMimetype.equals(writer.getMimetype())) + { + throw new AlfrescoRuntimeException("The target mimetype doesn't match the writer's mimetype: \n" + + " target mimetype: " + targetMimetype + "\n" + + " writer: " + writer); + } + transformer.transform(reader, writer, options); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/transform/ContentTransformer.java b/source/java/org/alfresco/repo/content/transform/ContentTransformer.java new file mode 100644 index 0000000000..2ff485803a --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/ContentTransformer.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.util.Map; + +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; + +/** + * Interface for class that allow content transformation from one mimetype to another. + * + * @author Derek Hulley + */ +public interface ContentTransformer +{ + /** + * Provides the approximate accuracy with which this transformer can + * transform from one mimetype to another. + *

    + * This method is used to determine, up front, which of a set of + * transformers will be used to perform a specific transformation. + * + * @param sourceMimetype the source mimetype + * @param targetMimetype the target mimetype + * @return Returns a score 0.0 to 1.0. 0.0 indicates that the + * transformation cannot be performed at all. 1.0 indicates that + * the transformation can be performed perfectly. + */ + public double getReliability(String sourceMimetype, String targetMimetype); + + /** + * Provides an estimate, usually a worst case guess, of how long a transformation + * will take. + *

    + * This method is used to determine, up front, which of a set of + * equally reliant transformers will be used for a specific transformation. + * + * @return Returns the approximate number of milliseconds per transformation + */ + public long getTransformationTime(); + + /** + * @see #transform(ContentReader, ContentWriter, Map) + */ + public void transform(ContentReader reader, ContentWriter writer) throws ContentIOException; + + /** + * Transforms the content provided by the reader and source mimetype + * to the writer and target mimetype. + *

    + * The transformation viability can be determined by an up front call + * to {@link #getReliability(String, String)}. + *

    + * The source and target mimetypes must be available on the + * {@link org.alfresco.service.cmr.repository.ContentAccessor#getMimetype()} methods of + * both the reader and the writer. + * + * @param reader the source of the content + * @param writer the destination of the transformed content + * @param options options to pass to the transformer. These are transformer dependent + * and may be null. + * @throws ContentIOException if an IO exception occurs + */ + public void transform( + ContentReader reader, + ContentWriter writer, + Map options) throws ContentIOException; +} diff --git a/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java b/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java new file mode 100644 index 0000000000..d46e3a6c2e --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; + +/** + * Holds and provides the most appropriate content transformer for + * a particular source and target mimetype transformation request. + *

    + * The transformers themselves are used to determine the applicability + * of a particular transformation. + * + * @see org.alfresco.repo.content.transform.ContentTransformer + * + * @author Derek Hulley + */ +public class ContentTransformerRegistry +{ + private static final Log logger = LogFactory.getLog(ContentTransformerRegistry.class); + + private List transformers; + private MimetypeMap mimetypeMap; + /** Cache of previously used transactions */ + private Map> transformationCache; + private short accessCount; + /** Controls read access to the transformation cache */ + private Lock transformationCacheReadLock; + /** controls write access to the transformation cache */ + private Lock transformationCacheWriteLock; + + /** + * @param mimetypeMap all the mimetypes available to the system + */ + public ContentTransformerRegistry(MimetypeMap mimetypeMap) + { + Assert.notNull(mimetypeMap, "The MimetypeMap is mandatory"); + this.mimetypeMap = mimetypeMap; + + this.transformers = Collections.emptyList(); // just in case it isn't set + transformationCache = new HashMap>(17); + + accessCount = 0; + // create lock objects for access to the cache + ReadWriteLock transformationCacheLock = new ReentrantReadWriteLock(); + transformationCacheReadLock = transformationCacheLock.readLock(); + transformationCacheWriteLock = transformationCacheLock.writeLock(); + } + + /** + * Provides a list of explicit transformers to use. + * + * @param transformations list of ( list of ( (from-mimetype)(to-mimetype)(transformer) ) ) + */ + public void setExplicitTransformations(List> transformations) + { + for (List list : transformations) + { + if (list.size() != 3) + { + throw new AlfrescoRuntimeException( + "Explicit transformation is 'from-mimetype', 'to-mimetype' and 'transformer': \n" + + " list: " + list); + } + try + { + String sourceMimetype = (String) list.get(0); + String targetMimetype = (String) list.get(1); + ContentTransformer transformer = (ContentTransformer) list.get(2); + // create the transformation + TransformationKey key = new TransformationKey(sourceMimetype, targetMimetype); + // bypass all discovery and plug this directly into the cache + transformationCache.put(key, Collections.singletonList(transformer)); + } + catch (ClassCastException e) + { + throw new AlfrescoRuntimeException( + "Explicit transformation is 'from-mimetype', 'to-mimetype' and 'transformer': \n" + + " list: " + list); + } + } + } + + /** + * Provides a list of self-discovering transformers that the registry will fall + * back on if a transformation is not available from the explicitly set + * transformations. + * + * @param transformers all the available transformers that the registry can + * work with + */ + public void setTransformers(List transformers) + { + this.transformers = transformers; + } + + /** + * Resets the transformation cache. This allows a fresh analysis of the best + * conversions based on actual average performance of the transformers. + */ + public void resetCache() + { + // get a write lock on the cache + transformationCacheWriteLock.lock(); + try + { + transformationCache.clear(); + accessCount = 0; + } + finally + { + transformationCacheWriteLock.unlock(); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Content transformation cache reset"); + } + } + + /** + * Gets the best transformer possible. This is a combination of the most reliable + * and the most performant transformer. + *

    + * The result is cached for quicker access next time. + * + * @param sourceMimetype the source mimetype of the transformation + * @param targetMimetype the target mimetype of the transformation + * @return Returns a content transformer that can perform the desired + * transformation or null if no transformer could be found that would do it. + */ + public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + // check that the mimetypes are valid + if (!mimetypeMap.getMimetypes().contains(sourceMimetype)) + { + throw new AlfrescoRuntimeException("Unknown source mimetype: " + sourceMimetype); + } + if (!mimetypeMap.getMimetypes().contains(targetMimetype)) + { + throw new AlfrescoRuntimeException("Unknown target mimetype: " + targetMimetype); + } + + TransformationKey key = new TransformationKey(sourceMimetype, targetMimetype); + List transformers = null; + transformationCacheReadLock.lock(); + try + { + if (transformationCache.containsKey(key)) + { + // the translation has been requested before + // it might have been null + transformers = transformationCache.get(key); + } + } + finally + { + transformationCacheReadLock.unlock(); + } + + if (transformers == null) + { + // the translation has not been requested before + // get a write lock on the cache + // no double check done as it is not an expensive task + transformationCacheWriteLock.lock(); + try + { + // find the most suitable transformer - may be empty list + transformers = findTransformers(sourceMimetype, targetMimetype); + // store the result even if it is null + transformationCache.put(key, transformers); + } + finally + { + transformationCacheWriteLock.unlock(); + } + } + // select the most performant transformer + long bestTime = -1L; + ContentTransformer bestTransformer = null; + for (ContentTransformer transformer : transformers) + { + long transformationTime = transformer.getTransformationTime(); + // is it better? + if (bestTransformer == null || transformationTime < bestTime) + { + bestTransformer = transformer; + bestTime = transformationTime; + } + } + // done + return bestTransformer; + } + + /** + * Gets all transformers, of equal reliability, that can perform the requested transformation. + * + * @return Returns best transformer for the translation - null if all + * score 0.0 on reliability + */ + private List findTransformers(String sourceMimetype, String targetMimetype) + { + // search for a simple transformer that can do the job + List transformers = findDirectTransformers(sourceMimetype, targetMimetype); + // get the complex transformers that can do the job + List complexTransformers = findComplexTransformer(sourceMimetype, targetMimetype); + transformers.addAll(complexTransformers); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Searched for transformer: \n" + + " source mimetype: " + sourceMimetype + "\n" + + " target mimetype: " + targetMimetype + "\n" + + " transformers: " + transformers); + } + return transformers; + } + + /** + * Loops through the content transformers and picks the ones with the highest reliabilities. + *

    + * Where there are several transformers that are equally reliable, they are all returned. + * + * @return Returns the most reliable transformers for the translation - empty list if there + * are none. + */ + private List findDirectTransformers(String sourceMimetype, String targetMimetype) + { + double maxReliability = 0.0; + long leastTime = 100000L; // 100 seconds - longer than anyone would think of waiting + List bestTransformers = new ArrayList(2); + // loop through transformers + for (ContentTransformer transformer : this.transformers) + { + double reliability = transformer.getReliability(sourceMimetype, targetMimetype); + if (reliability <= 0.0) + { + // it is unusable + continue; + } + else if (reliability < maxReliability) + { + // it is not the best one to use + continue; + } + else if (reliability == maxReliability) + { + // it is as reliable as a previous transformer + } + else + { + // it is better than any previous transformer - wipe them + bestTransformers.clear(); + maxReliability = reliability; + } + // add the transformer to the list + bestTransformers.add(transformer); + } + // done + return bestTransformers; + } + + /** + * Uses a list of known mimetypes to build transformations from several direct transformations. + */ + private List findComplexTransformer(String sourceMimetype, String targetMimetype) + { + // get a complete list of mimetypes + // TODO: Build complex transformers by searching for transformations by mimetype + return Collections.emptyList(); + } + + /** + * Recursive method to build up a list of content transformers + */ + private void buildTransformer(List transformers, + double reliability, + List touchedMimetypes, + String currentMimetype, + String targetMimetype) + { + throw new UnsupportedOperationException(); + } + + /** + * A key for a combination of a source and target mimetype + */ + public static class TransformationKey + { + private final String sourceMimetype; + private final String targetMimetype; + private final String key; + + public TransformationKey(String sourceMimetype, String targetMimetype) + { + this.key = (sourceMimetype + "_" + targetMimetype); + this.sourceMimetype = sourceMimetype; + this.targetMimetype = targetMimetype; + } + + public String getSourceMimetype() + { + return sourceMimetype; + } + public String getTargetMimetype() + { + return targetMimetype; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (this == obj) + { + return true; + } + else if (!(obj instanceof TransformationKey)) + { + return false; + } + TransformationKey that = (TransformationKey) obj; + return this.key.equals(that.key); + } + @Override + public int hashCode() + { + return key.hashCode(); + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistryTest.java b/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistryTest.java new file mode 100644 index 0000000000..7f24ac2155 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistryTest.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; + +/** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * + * @author Derek Hulley + */ +public class ContentTransformerRegistryTest extends AbstractContentTransformerTest +{ + private static final String A = MimetypeMap.MIMETYPE_TEXT_PLAIN; + private static final String B = MimetypeMap.MIMETYPE_XML; + private static final String C = MimetypeMap.MIMETYPE_WORD; + private static final String D = MimetypeMap.MIMETYPE_HTML; + + /** a real registry with real transformers */ + private ContentTransformerRegistry registry; + /** a fake registry with fake transformers */ + private ContentTransformerRegistry dummyRegistry; + + private ContentReader reader; + private ContentWriter writer; + + /** + * Allows dependency injection + */ + public void setContentTransformerRegistry(ContentTransformerRegistry registry) + { + this.registry = registry; + } + + @Override + public void onSetUpInTransaction() throws Exception + { + reader = new FileContentReader(TempFileProvider.createTempFile(getName(), ".txt")); + reader.setMimetype(A); + writer = new FileContentWriter(TempFileProvider.createTempFile(getName(), ".txt")); + writer.setMimetype(D); + + byte[] bytes = new byte[256]; + for (int i = 0; i < 256; i++) + { + bytes[i] = (byte)i; + } + List transformers = new ArrayList(5); + // create some dummy transformers for reliability tests + transformers.add(new DummyTransformer(A, B, 0.3, 10L)); + transformers.add(new DummyTransformer(A, B, 0.6, 10L)); + transformers.add(new DummyTransformer(A, C, 0.5, 10L)); + transformers.add(new DummyTransformer(A, C, 1.0, 10L)); + transformers.add(new DummyTransformer(B, C, 0.2, 10L)); + // create some dummy transformers for speed tests + transformers.add(new DummyTransformer(A, D, 1.0, 20L)); + transformers.add(new DummyTransformer(A, D, 1.0, 20L)); + transformers.add(new DummyTransformer(A, D, 1.0, 10L)); // the fast one + transformers.add(new DummyTransformer(A, D, 1.0, 20L)); + transformers.add(new DummyTransformer(A, D, 1.0, 20L)); + // create the dummyRegistry + dummyRegistry = new ContentTransformerRegistry(mimetypeMap); + dummyRegistry.setTransformers(transformers); + } + + /** + * Checks that required objects are present + */ + public void testSetUp() throws Exception + { + super.testSetUp(); + assertNotNull(registry); + } + + /** + * @return Returns the transformer provided by the real registry + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return registry.getTransformer(sourceMimetype, targetMimetype); + } + + public void testNullRetrieval() throws Exception + { + ContentTransformer transformer = null; + transformer = dummyRegistry.getTransformer(C, B); + assertNull("No transformer expected", transformer); + transformer = dummyRegistry.getTransformer(C, A); + assertNull("No transformer expected", transformer); + transformer = dummyRegistry.getTransformer(B, A); + assertNull("No transformer expected", transformer); + } + + public void testSimpleRetrieval() throws Exception + { + ContentTransformer transformer = null; + // B -> C expect 0.2 + transformer = dummyRegistry.getTransformer(B, C); + transformer = dummyRegistry.getTransformer(B, C); + assertNotNull("No transformer found", transformer); + assertEquals("Incorrect reliability", 0.2, transformer.getReliability(B, C)); + assertEquals("Incorrect reliability", 0.0, transformer.getReliability(C, B)); + } + + /** + * Force some equally reliant transformers to do some work and develop + * different average transformation times. Check that the registry + * copes with the new averages after a reset. + */ + public void testPerformanceRetrieval() throws Exception + { + // A -> D expect 1.0, 10ms + ContentTransformer transformer1 = dummyRegistry.getTransformer(A, D); + assertEquals("Incorrect reliability", 1.0, transformer1.getReliability(A, D)); + assertEquals("Incorrect reliability", 0.0, transformer1.getReliability(D, A)); + assertEquals("Incorrect transformation time", 10L, transformer1.getTransformationTime()); + } + + public void testScoredRetrieval() throws Exception + { + ContentTransformer transformer = null; + // A -> B expect 0.6 + transformer = dummyRegistry.getTransformer(A, B); + assertNotNull("No transformer found", transformer); + assertEquals("Incorrect reliability", 0.6, transformer.getReliability(A, B)); + assertEquals("Incorrect reliability", 0.0, transformer.getReliability(B, A)); + // A -> C expect 1.0 + transformer = dummyRegistry.getTransformer(A, C); + assertNotNull("No transformer found", transformer); + assertEquals("Incorrect reliability", 1.0, transformer.getReliability(A, C)); + assertEquals("Incorrect reliability", 0.0, transformer.getReliability(C, A)); + } + + /** + * Set an explicit, and bizarre, transformation. Check that it is used. + * + */ + public void testExplicitTransformation() + { + ContentTransformer dummyTransformer = new DummyTransformer( + MimetypeMap.MIMETYPE_FLASH, MimetypeMap.MIMETYPE_EXCEL, 1.0, 12345); + + List transform = new ArrayList(3); + transform.add(MimetypeMap.MIMETYPE_FLASH); + transform.add(MimetypeMap.MIMETYPE_EXCEL); + transform.add(dummyTransformer); + + List> explicitTransformers = Collections.singletonList(transform); + // add it to the registry + dummyRegistry.setExplicitTransformations(explicitTransformers); + + // get the appropriate transformer for the bizarre mapping + ContentTransformer checkTransformer = dummyRegistry.getTransformer( + MimetypeMap.MIMETYPE_FLASH, MimetypeMap.MIMETYPE_EXCEL); + + assertNotNull("No explicit transformer found", checkTransformer); + assertTrue("Expected explicit transformer", dummyTransformer == checkTransformer); + } + + /** + * Dummy transformer that does no transformation and scores exactly as it is + * told to in the constructor. It enables the tests to be sure of what to expect. + */ + private static class DummyTransformer extends AbstractContentTransformer + { + private String sourceMimetype; + private String targetMimetype; + private double reliability; + private long transformationTime; + + public DummyTransformer(String sourceMimetype, String targetMimetype, + double reliability, long transformationTime) + { + this.sourceMimetype = sourceMimetype; + this.targetMimetype = targetMimetype; + this.reliability = reliability; + this.transformationTime = transformationTime; + } + + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (this.sourceMimetype.equals(sourceMimetype) + && this.targetMimetype.equals(targetMimetype)) + { + return reliability; + } + else + { + return 0.0; + } + } + + /** + * Just notches up some average times + */ + public void transformInternal( + ContentReader reader, + ContentWriter writer, + Map options) throws Exception + { + // just update the transformation time + super.recordTime(transformationTime); + } + + /** + * @return Returns the fixed dummy average transformation time + */ + public synchronized long getTransformationTime() + { + return transformationTime; + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformer.java b/source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformer.java new file mode 100644 index 0000000000..a21317050b --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.htmlparser.beans.StringBean; + +/** + * @see http://htmlparser.sourceforge.net/ + * @see org.htmlparser.beans.StringBean + * + * @author Derek Hulley + */ +public class HtmlParserContentTransformer extends AbstractContentTransformer +{ + private static final Log logger = LogFactory.getLog(HtmlParserContentTransformer.class); + + /** + * Only support HTML to TEXT. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (!MimetypeMap.MIMETYPE_HTML.equals(sourceMimetype) || + !MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype)) + { + // only support HTML -> TEXT + return 0.0; + } + else + { + return 1.0; + } + } + + public void transformInternal(ContentReader reader, ContentWriter writer, Map options) + throws Exception + { + // we can only work from a file + File htmlFile = TempFileProvider.createTempFile("HtmlParserContentTransformer_", ".html"); + reader.getContent(htmlFile); + + // create the extractor + StringBean extractor = new StringBean(); + extractor.setCollapse(false); + extractor.setLinks(false); + extractor.setReplaceNonBreakingSpaces(false); + extractor.setURL(htmlFile.getAbsolutePath()); + + // get the text + String text = extractor.getStrings(); + // write it to the writer + writer.putContent(text); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformerTest.java new file mode 100644 index 0000000000..779ad2dd2d --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/HtmlParserContentTransformerTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import org.alfresco.repo.content.MimetypeMap; + +/** + * @see org.alfresco.repo.content.transform.HtmlParserContentTransformer + * + * @author Derek Hulley + */ +public class HtmlParserContentTransformerTest extends AbstractContentTransformerTest +{ + private static final String SOME_CONTENT = "azAz10!£$%^&*()\t\r\n"; + + private ContentTransformer transformer; + + @Override + public void onSetUpInTransaction() throws Exception + { + transformer = new HtmlParserContentTransformer(); + } + + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testSetUp() throws Exception + { + assertNotNull(transformer); + } + + public void checkReliability() throws Exception + { + // check reliability + double reliability = transformer.getReliability(MimetypeMap.MIMETYPE_HTML, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Reliability incorrect", 1.0, reliability); // plain text to plain text is 100% + + // check other way around + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_HTML); + assertEquals("Reliability incorrect", 0.0, reliability); // plain text to plain text is 0% + } +} diff --git a/source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformer.java b/source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformer.java new file mode 100644 index 0000000000..530b5692f4 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformer.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.IOException; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.pdfbox.pdmodel.PDDocument; +import org.pdfbox.util.PDFTextStripper; + +/** + * Makes use of the {@link http://www.pdfbox.org/ PDFBox} library to + * perform conversions from PDF files to text. + * + * @author Derek Hulley + */ +public class PdfBoxContentTransformer extends AbstractContentTransformer +{ + private static final Log logger = LogFactory.getLog(PdfBoxContentTransformer.class); + + /** + * Currently the only transformation performed is that of text extraction from PDF documents. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + // TODO: Expand PDFBox usage to convert images to PDF and investigate other conversions + + if (!MimetypeMap.MIMETYPE_PDF.equals(sourceMimetype) || + !MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype)) + { + // only support PDF -> Text + return 0.0; + } + else + { + return 1.0; + } + } + + public void transformInternal(ContentReader reader, ContentWriter writer, Map options) + { + PDDocument pdf = null; + try + { + // stream the document in + pdf = PDDocument.load(reader.getContentInputStream()); + // strip the text out + PDFTextStripper stripper = new PDFTextStripper(); + String text = stripper.getText(pdf); + + // dump it all to the writer + writer.putContent(text); + } + catch (IOException e) + { + throw new ContentIOException("PDF text stripping failed: \n" + + " reader: " + reader); + } + finally + { + if (pdf != null) + { + try { pdf.close(); } catch (Throwable e) {e.printStackTrace(); } + } + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformerTest.java new file mode 100644 index 0000000000..f8033bd243 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/PdfBoxContentTransformerTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import org.alfresco.repo.content.MimetypeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.PdfBoxContentTransformer + * + * @author Derek Hulley + */ +public class PdfBoxContentTransformerTest extends AbstractContentTransformerTest +{ + private static final Log logger = LogFactory.getLog(PdfBoxContentTransformerTest.class); + + private ContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + transformer = new PdfBoxContentTransformer(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_PDF); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_PDF, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should be supported", 1.0, reliability); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformer.java b/source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformer.java new file mode 100644 index 0000000000..09ff06dc76 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformer.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.OutputStream; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + +/** + * Makes use of the {@link http://jakarta.apache.org/poi/ POI} library to + * perform conversions from Excel spreadsheets to text (comma separated). + *

    + * While most text extraction from spreadsheets only extract the first sheet of + * the workbook, the method used here extracts the text from all the sheets. + * This is more useful, especially when it comes to indexing spreadsheets. + *

    + * In the case where there is only one sheet in the document, the results will be + * exactly the same as most extractors. Where there are multiple sheets, the results + * will differ, but meaningful reimporting of the text document is not possible + * anyway. + * + * @author Derek Hulley + */ +public class PoiHssfContentTransformer extends AbstractContentTransformer +{ + /** + * Windows carriage return line feed pair. + */ + private static final String LINE_BREAK = "\r\n"; + + private static final Log logger = LogFactory.getLog(PoiHssfContentTransformer.class); + + /** + * Currently the only transformation performed is that of text extraction from XLS documents. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (!MimetypeMap.MIMETYPE_EXCEL.equals(sourceMimetype) || + !MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype)) + { + // only support XLS -> Text + return 0.0; + } + else + { + return 1.0; + } + } + + public void transformInternal(ContentReader reader, ContentWriter writer, Map options) + throws Exception + { + OutputStream os = writer.getContentOutputStream(); + String encoding = writer.getEncoding(); + try + { + // open the workbook + HSSFWorkbook workbook = new HSSFWorkbook(reader.getContentInputStream()); + // how many sheets are there? + int sheetCount = workbook.getNumberOfSheets(); + // transform each sheet + for (int i = 0; i < sheetCount; i++) + { + HSSFSheet sheet = workbook.getSheetAt(i); + String sheetName = workbook.getSheetName(i); + writeSheet(os, sheet, encoding); + // write the sheet name + PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false); + PoiHssfContentTransformer.writeString(os, encoding, "End of sheet: " + sheetName, true); + PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false); + PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false); + } + } + finally + { + if (os != null) + { + try { os.close(); } catch (Throwable e) {} + } + } + } + + /** + * Dumps the text from the sheet to the stream in CSV format + */ + private void writeSheet(OutputStream os, HSSFSheet sheet, String encoding) throws Exception + { + int rows = sheet.getLastRowNum(); + // transform each row + for (int i = 0; i <= rows; i++) + { + HSSFRow row = sheet.getRow(i); + if (row != null) + { + writeRow(os, row, encoding); + } + // break between rows + if (i < rows) + { + PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false); + } + } + } + + private void writeRow(OutputStream os, HSSFRow row, String encoding) throws Exception + { + short firstCellNum = row.getFirstCellNum(); + short lastCellNum = row.getLastCellNum(); + // pad out to first cell + for (short i = 0; i < firstCellNum; i++) + { + PoiHssfContentTransformer.writeString(os, encoding, ",", false); // CSV up to first cell + } + // write each cell + for (short i = 0; i <= lastCellNum; i++) + { + HSSFCell cell = row.getCell(i); + if (cell != null) + { + StringBuilder sb = new StringBuilder(10); + switch (cell.getCellType()) + { + case HSSFCell.CELL_TYPE_BLANK: + // ignore + break; + case HSSFCell.CELL_TYPE_BOOLEAN: + sb.append(cell.getBooleanCellValue()); + break; + case HSSFCell.CELL_TYPE_ERROR: + sb.append("ERROR"); + break; + case HSSFCell.CELL_TYPE_FORMULA: + double dataNumber = cell.getNumericCellValue(); + if (Double.isNaN(dataNumber)) + { + // treat it as a string + sb.append(cell.getStringCellValue()); + } + else + { + // treat it as a number + sb.append(dataNumber); + } + break; + case HSSFCell.CELL_TYPE_NUMERIC: + sb.append(cell.getNumericCellValue()); + break; + case HSSFCell.CELL_TYPE_STRING: + sb.append(cell.getStringCellValue()); + break; + default: + throw new RuntimeException("Unknown HSSF cell type: " + cell); + } + String data = sb.toString(); + PoiHssfContentTransformer.writeString(os, encoding, data, true); + } + // comma separate if required + if (i < lastCellNum) + { + PoiHssfContentTransformer.writeString(os, encoding, ",", false); + } + } + } + + /** + * Writes the given data to the stream using the encoding specified. If the encoding + * is not given, the default String to byte[] conversion will be + * used. + *

    + * The given data string will be escaped appropriately. + * + * @param os the stream to write to + * @param encoding the encoding to use, or null if the default encoding is acceptable + * @param value the string to write + * @param isData true if the value represents a human-readable string, false if the + * value represents formatting characters, separating characters, etc. + * @throws Exception + */ + public static void writeString(OutputStream os, String encoding, String value, boolean isData) throws Exception + { + if (value == null) + { + // nothing to do + return; + } + int dataLength = value.length(); + if (dataLength == 0) + { + // nothing to do + return; + } + + // escape the string + StringBuilder sb = new StringBuilder(dataLength + 5); // slightly longer than the data + for (int i = 0; i < dataLength; i++) + { + char currentChar = value.charAt(i); + if (currentChar == '\"') // inverted commas + { + sb.append("\""); // CSV escaping of inverted commas + } + // append the char + sb.append(currentChar); + } + // enclose in inverted commas for safety + if (isData) + { + sb.insert(0, "\""); + sb.append("\""); + } + // escaping complete + value = sb.toString(); + + byte[] bytes = null; + if (encoding == null) + { + // use default encoding + bytes = value.getBytes(); + } + else + { + bytes = value.getBytes(encoding); + } + // write to the stream + os.write(bytes); + // done + } +} diff --git a/source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformerTest.java new file mode 100644 index 0000000000..238526b92c --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/PoiHssfContentTransformerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.io.InputStream; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.PoiHssfContentTransformer + * + * @author Derek Hulley + */ +public class PoiHssfContentTransformerTest extends AbstractContentTransformerTest +{ + private static final Log logger = LogFactory.getLog(PoiHssfContentTransformerTest.class); + + private ContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + transformer = new PoiHssfContentTransformer(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_EXCEL); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should be supported", 1.0, reliability); + } + + /** + * Tests a specific failure in the library + */ + public void xtestBugFixAR114() throws Exception + { + File tempFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_" + getName() + "_", + ".xls"); + FileContentWriter writer = new FileContentWriter(tempFile); + writer.setMimetype(MimetypeMap.MIMETYPE_EXCEL); + // get the test resource and write it (Excel) + InputStream is = getClass().getClassLoader().getResourceAsStream("Plan270904b.xls"); + assertNotNull("Test resource not found: Plan270904b.xls"); + writer.putContent(is); + + // get the source of the transformation + ContentReader reader = writer.getReader(); + + // make a new location of the transform output (plain text) + tempFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_" + getName() + "_", + ".txt"); + writer = new FileContentWriter(tempFile); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + + // transform it + transformer.transform(reader, writer); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformer.java b/source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformer.java new file mode 100644 index 0000000000..226bb59277 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformer.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.util.TempFileProvider; +import org.alfresco.util.exec.RuntimeExec; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This configurable wrapper is able to execute any command line transformation that + * accepts an input and an output file on the command line. + *

    + * The following parameters are use: + *

      + *
    • {@link #VAR_SOURCE target} - full path to the source file
    • + *
    • {@link #VAR_TARGET source} - full path to the target file
    • + *
    + * Provided that the command executed ultimately transforms the source file + * and leaves the result in the target file, the transformation should be + * successful. + *

    + * NOTE: It is only the contents of the files that can be transformed. + * Any attempt to modify the source or target file metadata will, at best, have + * no effect, but may ultimately lead to the transformation failing. This is + * because the files provided are both temporary files that reside in a location + * outside the system's content store. + * + * @see org.alfresco.util.exec.RuntimeExec + * + * @since 1.1 + * @author Derek Hulley + */ +public class RuntimeExecutableContentTransformer extends AbstractContentTransformer +{ + public static final String VAR_SOURCE = "source"; + public static final String VAR_TARGET = "target"; + + private static Log logger = LogFactory.getLog(RuntimeExecutableContentTransformer.class); + + private boolean available; + private MimetypeService mimetypeService; + private RuntimeExec checkCommand; + private RuntimeExec transformCommand; + private Set errCodes; + + public RuntimeExecutableContentTransformer() + { + this.errCodes = new HashSet(2); + errCodes.add(1); + errCodes.add(2); + } + + /** + * @param mimetypeService the mapping from mimetype to extensions + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + /** + * Set the runtime executer that will be called as part of the initialisation + * to determine if the transformer is able to function. This is optional, but allows + * the transformer registry to detect and avoid using this instance if it is not working. + *

    + * The command will be considered to have failed if the + * + * @param checkCommand the initialisation check command + */ + public void setCheckCommand(RuntimeExec checkCommand) + { + this.checkCommand = checkCommand; + } + + /** + * Set the runtime executer that will called to perform the actual transformation. + * + * @param transformCommand the runtime transform command + */ + public void setTransformCommand(RuntimeExec transformCommand) + { + this.transformCommand = transformCommand; + } + + /** + * A comma or space separated list of values that, if returned by the executed command, + * indicate an error value. This defaults to "1, 2". + * + * @param erroCodesStr + */ + public void setErrorCodes(String errCodesStr) + { + StringTokenizer tokenizer = new StringTokenizer(errCodesStr, " ,"); + while(tokenizer.hasMoreElements()) + { + String errCodeStr = tokenizer.nextToken(); + // attempt to convert it to an integer + try + { + int errCode = Integer.parseInt(errCodeStr); + this.errCodes.add(errCode); + } + catch (NumberFormatException e) + { + throw new AlfrescoRuntimeException("Error codes string must be integers: " + errCodesStr); + } + } + } + + /** + * @param exitValue the command exit value + * @return Returns true if the code is a listed failure code + * + * @see #setErrorCodes(String) + */ + private boolean isFailureCode(int exitValue) + { + return errCodes.contains((Integer)exitValue); + } + + /** + * Executes the check command, if present. Any errors will result in this component + * being rendered unusable within the transformer registry, but may still be called + * directly. + */ + public void init() + { + if (transformCommand == null) + { + throw new AlfrescoRuntimeException("Mandatory property 'transformCommand' not set"); + } + else if (mimetypeService == null) + { + throw new AlfrescoRuntimeException("Mandatory property 'mimetypeService' not set"); + } + + // execute the command + if (checkCommand != null) + { + ExecutionResult result = checkCommand.execute(); + // check the return code + available = !isFailureCode(result.getExitValue()); + } + else + { + // no check - just assume it is available + available = true; + } + } + + /** + * Unless otherwise configured, this component supports all mimetypes. + * If the {@link #init() initialization} failed, + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (!available) + { + return 0.0; + } + else + { + return 1.0; + } + } + + /** + * Converts the source and target content to temporary files with the + * correct extensions for the mimetype that they map to. + * + * @see #transformInternal(File, File) + */ + protected final void transformInternal( + ContentReader reader, + ContentWriter writer, + Map options) throws Exception + { + // get mimetypes + String sourceMimetype = getMimetype(reader); + String targetMimetype = getMimetype(writer); + + // get the extensions to use + String sourceExtension = mimetypeService.getExtension(sourceMimetype); + String targetExtension = mimetypeService.getExtension(targetMimetype); + if (sourceExtension == null || targetExtension == null) + { + throw new AlfrescoRuntimeException("Unknown extensions for mimetypes: \n" + + " source mimetype: " + sourceMimetype + "\n" + + " source extension: " + sourceExtension + "\n" + + " target mimetype: " + targetMimetype + "\n" + + " target extension: " + targetExtension); + } + + // if the source mimetype is the same as the target's then just stream it + if (sourceMimetype.equals(targetMimetype)) + { + writer.putContent(reader.getContentInputStream()); + return; + } + + // create required temp files + File sourceFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_source_", + "." + sourceExtension); + File targetFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_target_", + "." + targetExtension); + + Map properties = new HashMap(5); + // copy options over + for (Map.Entry entry : options.entrySet()) + { + String key = entry.getKey(); + Object value = entry.getValue(); + properties.put(key, (value == null ? null : value.toString())); + } + // add the source and target properties + properties.put(VAR_SOURCE, sourceFile.getAbsolutePath()); + properties.put(VAR_TARGET, targetFile.getAbsolutePath()); + + // pull reader file into source temp file + reader.getContent(sourceFile); + + // execute the transformation command + ExecutionResult result = null; + try + { + result = transformCommand.execute(properties); + } + catch (Throwable e) + { + throw new ContentIOException("Transformation failed during command execution: \n" + transformCommand, e); + } + + // check + if (isFailureCode(result.getExitValue())) + { + throw new ContentIOException("Transformation failed - status indicates an error: \n" + result); + } + + // check that the file was created + if (!targetFile.exists()) + { + throw new ContentIOException("Transformation failed - target file doesn't exist: \n" + result); + } + // copy the target file back into the repo + writer.putContent(targetFile); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Transformation completed: \n" + + " source: " + reader + "\n" + + " target: " + writer + "\n" + + " options: " + options + "\n" + + " result: \n" + result); + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformerTest.java new file mode 100644 index 0000000000..51b2be6e12 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/RuntimeExecutableContentTransformerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.BaseAlfrescoTestCase; +import org.alfresco.util.TempFileProvider; +import org.alfresco.util.exec.RuntimeExec; + +/** + * @see org.alfresco.repo.content.transform.RuntimeExecutableContentTransformer + * + * @author Derek Hulley + */ +public class RuntimeExecutableContentTransformerTest extends BaseAlfrescoTestCase +{ + private RuntimeExecutableContentTransformer transformer; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + transformer = new RuntimeExecutableContentTransformer(); + // the command to execute + RuntimeExec transformCommand = new RuntimeExec(); + Map commandMap = new HashMap(5); + commandMap.put("Linux", "mv -f ${source} ${target}"); + commandMap.put("*", "cmd /c copy /Y \"${source}\" \"${target}\""); + transformCommand.setCommandMap(commandMap); + transformer.setTransformCommand(transformCommand); + transformer.setMimetypeService(serviceRegistry.getMimetypeService()); + transformer.setErrorCodes("1, 2"); + + // initialise so that it doesn't score 0 + transformer.init(); + } + + public void testCopyCommand() throws Exception + { + String content = ""; + // create the source + File sourceFile = TempFileProvider.createTempFile(getName() + "_", ".txt"); + ContentWriter tempWriter = new FileContentWriter(sourceFile); + tempWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + tempWriter.putContent(content); + ContentReader reader = tempWriter.getReader(); + // create the target + File targetFile = TempFileProvider.createTempFile(getName() + "_", ".xml"); + ContentWriter writer = new FileContentWriter(targetFile); + writer.setMimetype(MimetypeMap.MIMETYPE_XML); + + // do the transformation + transformer.transform(reader, writer); // no options on the copy + + // make sure that the content was copied over + ContentReader checkReader = writer.getReader(); + String checkContent = checkReader.getContentString(); + assertEquals("Content not copied", content, checkContent); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformer.java b/source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformer.java new file mode 100644 index 0000000000..79eb841f09 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformer.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Converts any textual format to plain text. + *

    + * The transformation is sensitive to the source and target string encodings. + * + * @author Derek Hulley + */ +public class StringExtractingContentTransformer extends AbstractContentTransformer +{ + public static final String PREFIX_TEXT = "text/"; + + private static final Log logger = LogFactory.getLog(StringExtractingContentTransformer.class); + + /** + * Gives a high reliability for all translations from text/sometype to + * text/plain. As the text formats are already text, the characters + * are preserved and no actual conversion takes place. + *

    + * Extraction of text from binary data is wholly unreliable. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (!targetMimetype.equals(MimetypeMap.MIMETYPE_TEXT_PLAIN)) + { + // can only convert to plain text + return 0.0; + } + else if (sourceMimetype.equals(MimetypeMap.MIMETYPE_TEXT_PLAIN)) + { + // conversions from any plain text format are very reliable + return 1.0; + } + else if (sourceMimetype.startsWith(PREFIX_TEXT)) + { + // the source is text, but probably with some kind of markup + return 0.1; + } + else + { + // extracting text from binary is not useful + return 0.0; + } + } + + /** + * Text to text conversions are done directly using the content reader and writer string + * manipulation methods. + *

    + * Extraction of text from binary content attempts to take the possible character + * encoding into account. The text produced from this will, if the encoding was correct, + * be unformatted but valid. + */ + @Override + public void transformInternal(ContentReader reader, ContentWriter writer, Map options) + throws Exception + { + // is this a straight text-text transformation + transformText(reader, writer); + } + + /** + * Transformation optimized for text-to-text conversion + */ + private void transformText(ContentReader reader, ContentWriter writer) throws Exception + { + // get a char reader and writer + Reader charReader = null; + Writer charWriter = null; + try + { + if (reader.getEncoding() == null) + { + charReader = new InputStreamReader(reader.getContentInputStream()); + } + else + { + charReader = new InputStreamReader(reader.getContentInputStream(), reader.getEncoding()); + } + if (writer.getEncoding() == null) + { + charWriter = new OutputStreamWriter(writer.getContentOutputStream()); + } + else + { + charWriter = new OutputStreamWriter(writer.getContentOutputStream(), writer.getEncoding()); + } + // copy from the one to the other + char[] buffer = new char[1024]; + int readCount = 0; + while (readCount > -1) + { + // write the last read count number of bytes + charWriter.write(buffer, 0, readCount); + // fill the buffer again + readCount = charReader.read(buffer); + } + } + finally + { + if (charReader != null) + { + try { charReader.close(); } catch (Throwable e) { logger.error(e); } + } + if (charWriter != null) + { + try { charWriter.close(); } catch (Throwable e) { logger.error(e); } + } + } + // done + } +} diff --git a/source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformerTest.java new file mode 100644 index 0000000000..ef77942584 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/StringExtractingContentTransformerTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Random; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; + +/** + * @see org.alfresco.repo.content.transform.StringExtractingContentTransformer + * + * @author Derek Hulley + */ +public class StringExtractingContentTransformerTest extends AbstractContentTransformerTest +{ + private static final String SOME_CONTENT = "azAz10!£$%^&*()\t\r\n"; + + private ContentTransformer transformer; + /** the final destination of transformations */ + private ContentWriter targetWriter; + + @Override + public void onSetUpInTransaction() throws Exception + { + transformer = new StringExtractingContentTransformer(); + targetWriter = new FileContentWriter(getTempFile()); + targetWriter.setMimetype("text/plain"); + targetWriter.setEncoding("UTF-8"); + } + + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testSetUp() throws Exception + { + assertNotNull(transformer); + } + + /** + * @return Returns a new temp file + */ + private File getTempFile() + { + return TempFileProvider.createTempFile(getName(), ".txt"); + } + + /** + * Writes some content using the mimetype and encoding specified. + * + * @param mimetype + * @param encoding + * @return Returns a reader onto the newly written content + */ + private ContentReader writeContent(String mimetype, String encoding) + { + ContentWriter writer = new FileContentWriter(getTempFile()); + writer.setMimetype(mimetype); + writer.setEncoding(encoding); + // put content + writer.putContent(SOME_CONTENT); + // return a reader onto the new content + return writer.getReader(); + } + + public void testDirectTransform() throws Exception + { + ContentReader reader = writeContent("text/plain", "latin1"); + + // check reliability + double reliability = transformer.getReliability(reader.getMimetype(), targetWriter.getMimetype()); + assertEquals("Reliability incorrect", 1.0, reliability); // plain text to plain text is 100% + + // transform + transformer.transform(reader, targetWriter); + + // get a reader onto the transformed content and check + ContentReader checkReader = targetWriter.getReader(); + String checkContent = checkReader.getContentString(); + assertEquals("Content check failed", SOME_CONTENT, checkContent); + } + + public void testInterTextTransform() throws Exception + { + ContentReader reader = writeContent("text/xml", "UTF-16"); + + // check reliability + double reliability = transformer.getReliability(reader.getMimetype(), targetWriter.getMimetype()); + assertEquals("Reliability incorrect", 0.1, reliability); // markup to plain text not 100% + + // transform + transformer.transform(reader, targetWriter); + + // get a reader onto the transformed content and check + ContentReader checkReader = targetWriter.getReader(); + String checkContent = checkReader.getContentString(); + assertEquals("Content check failed", SOME_CONTENT, checkContent); + } + + /** + * Generate a large file and then transform it using the text extractor. + * We are not creating super-large file (1GB) in order to test the transform + * as it takes too long to create the file in the first place. Rather, + * this test can be used during profiling to ensure that memory is not + * being consumed. + */ + public void testLargeFileStreaming() throws Exception + { + File sourceFile = TempFileProvider.createTempFile(getName(), ".txt"); + + int chars = 1000000; // a million characters should do the trick + Random random = new Random(); + + Writer charWriter = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(sourceFile))); + for (int i = 0; i < chars; i++) + { + char next = (char)(random.nextDouble() * 93D + 32D); + charWriter.write(next); + } + charWriter.close(); + + // get a reader and a writer + ContentReader reader = new FileContentReader(sourceFile); + reader.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + + File outputFile = TempFileProvider.createTempFile(getName(), ".txt"); + ContentWriter writer = new FileContentWriter(outputFile); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + + // transform + transformer.transform(reader, writer); + + // delete files + sourceFile.delete(); + outputFile.delete(); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/TextMiningContentTransformer.java b/source/java/org/alfresco/repo/content/transform/TextMiningContentTransformer.java new file mode 100644 index 0000000000..8e33e060c5 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/TextMiningContentTransformer.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.textmining.text.extraction.WordExtractor; + +/** + * Makes use of the {@link http://www.textmining.org/ TextMining} library to + * perform conversions from MSWord documents to text. + * + * @author Derek Hulley + */ +public class TextMiningContentTransformer extends AbstractContentTransformer +{ + private static final Log logger = LogFactory.getLog(TextMiningContentTransformer.class); + + private WordExtractor wordExtractor; + + public TextMiningContentTransformer() + { + this.wordExtractor = new WordExtractor(); + } + + /** + * Currently the only transformation performed is that of text extraction from Word documents. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (!MimetypeMap.MIMETYPE_WORD.equals(sourceMimetype) || + !MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype)) + { + // only support DOC -> Text + return 0.0; + } + else + { + return 1.0; + } + } + + public void transformInternal(ContentReader reader, ContentWriter writer, Map options) + throws Exception + { + InputStream is = reader.getContentInputStream(); + String text = null; + try + { + text = wordExtractor.extractText(is); + } + catch (IOException e) + { + // check if this is an error caused by the fact that the .doc is in fact + // one of Word's temp non-documents + if (e.getMessage().contains("Unable to read entire header")) + { + // just assign an empty string + text = ""; + } + } + // dump the text out + writer.putContent(text); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/TextMiningContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/TextMiningContentTransformerTest.java new file mode 100644 index 0000000000..d6a1b093b4 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/TextMiningContentTransformerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.io.InputStream; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.TextMiningContentTransformer + * + * @author Derek Hulley + */ +public class TextMiningContentTransformerTest extends AbstractContentTransformerTest +{ + private static final Log logger = LogFactory.getLog(TextMiningContentTransformerTest.class); + + private ContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + transformer = new TextMiningContentTransformer(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_WORD); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should be supported", 1.0, reliability); + } + + /** + * Tests a specific failure in the library + */ + public void testBugFixAR1() throws Exception + { + File tempFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_" + getName() + "_", + ".doc"); + FileContentWriter writer = new FileContentWriter(tempFile); + writer.setMimetype(MimetypeMap.MIMETYPE_WORD); + // get the test resource and write it (MS Word) + InputStream is = getClass().getClassLoader().getResourceAsStream("farmers_markets_list_2003.doc"); + assertNotNull("Test resource not found: farmers_markets_list_2003.doc"); + writer.putContent(is); + + // get the source of the transformation + ContentReader reader = writer.getReader(); + + // make a new location of the transform output (plain text) + tempFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_" + getName() + "_", + ".txt"); + writer = new FileContentWriter(tempFile); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + + // transform it + transformer.transform(reader, writer); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/UnoContentTransformer.java b/source/java/org/alfresco/repo/content/transform/UnoContentTransformer.java new file mode 100644 index 0000000000..e01bad7702 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/UnoContentTransformer.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.util.HashMap; +import java.util.Map; + +import net.sf.joott.uno.DocumentConverter; +import net.sf.joott.uno.DocumentFormat; +import net.sf.joott.uno.UnoConnection; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; + +/** + * Makes use of the OpenOffice Uno interfaces to convert the content. + *

    + * The conversions are slow but reliable. + * + * @author Derek Hulley + */ +public class UnoContentTransformer extends AbstractContentTransformer +{ + /** map of DocumentFormat instances keyed by mimetype conversion */ + private static Map formatsByConversion; + + static + { + // Build the map of known Uno document formats and store by conversion key + formatsByConversion = new HashMap(17); + + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_HTML), + new DocumentFormatWrapper(DocumentFormat.HTML_WRITER, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_PDF), + new DocumentFormatWrapper(DocumentFormat.PDF_WRITER, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_WORD), + new DocumentFormatWrapper(DocumentFormat.TEXT, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_TEXT_PLAIN), + new DocumentFormatWrapper(DocumentFormat.TEXT, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_PDF), + new DocumentFormatWrapper(DocumentFormat.PDF_WRITER, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN), + new DocumentFormatWrapper(DocumentFormat.TEXT_CALC, 0.8)); // only first sheet extracted + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_PDF), + new DocumentFormatWrapper(DocumentFormat.PDF_CALC, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_FLASH), + new DocumentFormatWrapper(DocumentFormat.FLASH_IMPRESS, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_PDF), + new DocumentFormatWrapper(DocumentFormat.PDF_IMPRESS, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_HTML), + new DocumentFormatWrapper(DocumentFormat.HTML_WRITER, 1.0)); + formatsByConversion.put( + new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_HTML, MimetypeMap.MIMETYPE_PDF), + new DocumentFormatWrapper(DocumentFormat.PDF_WRITER_WEB, 1.0)); + + // there are many more formats available and therefore many more transformation combinations possible +// DocumentFormat.FLASH_IMPRESS +// DocumentFormat.HTML_CALC +// DocumentFormat.HTML_WRITER +// DocumentFormat.MS_EXCEL_97 +// DocumentFormat.MS_POWERPOINT_97 +// DocumentFormat.MS_WORD_97 +// DocumentFormat.PDF_CALC +// DocumentFormat.PDF_IMPRESS +// DocumentFormat.PDF_WRITER +// DocumentFormat.PDF_WRITER_WEB +// DocumentFormat.RTF +// DocumentFormat.TEXT +// DocumentFormat.TEXT_CALC +// DocumentFormat.XML_CALC +// DocumentFormat.XML_IMPRESS +// DocumentFormat.XML_WRITER +// DocumentFormat.XML_WRITER_WEB + } + + private String connectionUrl = UnoConnection.DEFAULT_CONNECTION_STRING; + private UnoConnection connection; + private boolean isConnected; + + /** + * Constructs the default transformer that will attempt to connect to the + * Uno server using the default connect string. + * + * @see UnoConnection#DEFAULT_CONNECTION_STRING + */ + public UnoContentTransformer() + { + } + + /** + * Override the default connection URL with a new one. + * + * @param connectionUrl the connection string + * + * @see UnoConnection#DEFAULT_CONNECTION_STRING + */ + public void setConnectionUrl(String connectionUrl) + { + this.connectionUrl = connectionUrl; + } + + /** + * Perform bean initialization + */ + public synchronized void init() + { + connection = new UnoConnection(connectionUrl); + // attempt to make an connection + try + { + connection.connect(); + isConnected = true; + } + catch (ConnectException e) + { + isConnected = false; + } + } + + /** + * @return Returns true if a connection to the Uno server could be established + */ + public boolean isConnected() + { + return isConnected; + } + + /** + * @param sourceMimetype + * @param targetMimetype + * @return Returns a document format wrapper that is valid for the given source and target mimetypes + */ + private static DocumentFormatWrapper getDocumentFormatWrapper(String sourceMimetype, String targetMimetype) + { + // get the well-known document format for the specific conversion + ContentTransformerRegistry.TransformationKey key = + new ContentTransformerRegistry.TransformationKey(sourceMimetype, targetMimetype); + DocumentFormatWrapper wrapper = UnoContentTransformer.formatsByConversion.get(key); + return wrapper; + } + + /** + * Checks how reliable the conversion will be when performed by the Uno server. + *

    + * The connection for the Uno server is checked in order to have any chance of + * being reliable. + *

    + * The conversions' reliabilities are set up statically based on prior tests that + * included checking performance as well as accuracy. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + // check if a connection to the Uno server can be established + if (!isConnected()) + { + // no connection means that conversion is not possible + return 0.0; + } + // check if the source and target mimetypes are supported + DocumentFormatWrapper docFormatWrapper = getDocumentFormatWrapper(sourceMimetype, targetMimetype); + if (docFormatWrapper == null) + { + return 0.0; + } + else + { + return docFormatWrapper.getReliability(); + } + } + + public void transformInternal(ContentReader reader, ContentWriter writer, Map options) + throws Exception + { + String sourceMimetype = getMimetype(reader); + String targetMimetype = getMimetype(writer); + + // create temporary files to convert from and to + File tempFromFile = TempFileProvider.createTempFile( + "UnoContentTransformer", + "." + getMimetypeService().getExtension(sourceMimetype)); + File tempToFile = TempFileProvider.createTempFile( + "UnoContentTransformer", + "." + getMimetypeService().getExtension(targetMimetype)); + // download the content from the source reader + reader.getContent(tempFromFile); + + // get the document format that should be used + DocumentFormatWrapper docFormatWrapper = getDocumentFormatWrapper(sourceMimetype, targetMimetype); + try + { + docFormatWrapper.execute(tempFromFile, tempToFile, connection); + // conversion success + } + catch (ConnectException e) + { + throw new ContentIOException("Connection to Uno server failed: \n" + + " reader: " + reader + "\n" + + " writer: " + writer, + e); + } + catch (IOException e) + { + throw new ContentIOException("Uno server conversion failed: \n" + + " reader: " + reader + "\n" + + " writer: " + writer + "\n" + + " from file: " + tempFromFile + "\n" + + " to file: " + tempToFile, + e); + } + + // upload the temp output to the writer given us + writer.putContent(tempToFile); + } + + /** + * Wraps a document format as well the reliability. The source and target mimetypes + * are not kept, but will probably be closely associated with the reliability. + */ + private static class DocumentFormatWrapper + { + /* + * Source and target mimetypes not kept -> class is private as it doesn't keep + * enough info to be used safely externally + */ + + private DocumentFormat documentFormat; + private double reliability; + + public DocumentFormatWrapper(DocumentFormat documentFormat, double reliability) + { + this.documentFormat = documentFormat; + this.reliability = reliability; + } + + public double getReliability() + { + return reliability; + } + + /** + * Executs the transformation + */ + public void execute(File fromFile, File toFile, UnoConnection connection) throws ConnectException, IOException + { + DocumentConverter converter = new DocumentConverter(connection); + converter.convert(fromFile, toFile, documentFormat); + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/UnoContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/UnoContentTransformerTest.java new file mode 100644 index 0000000000..afc080e145 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/UnoContentTransformerTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform; + +import org.alfresco.repo.content.MimetypeMap; + +/** + * @see org.alfresco.repo.content.transform.UnoContentTransformer + * + * @author Derek Hulley + */ +public class UnoContentTransformerTest extends AbstractContentTransformerTest +{ + private static String MIMETYPE_RUBBISH = "text/rubbish"; + + private UnoContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + transformer = new UnoContentTransformer(); + transformer.setMimetypeService(mimetypeMap); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testSetUp() throws Exception + { + super.testSetUp(); + assertNotNull(mimetypeMap); + } + + public void testReliability() throws Exception + { + if (!transformer.isConnected()) + { + // no connection + return; + } + double reliability = 0.0; + reliability = transformer.getReliability(MIMETYPE_RUBBISH, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MIMETYPE_RUBBISH); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_WORD); + assertEquals("Mimetype should be supported", 1.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should be supported", 1.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should be supported", 0.8, reliability); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/magick/AbstractImageMagickContentTransformer.java b/source/java/org/alfresco/repo/content/transform/magick/AbstractImageMagickContentTransformer.java new file mode 100644 index 0000000000..8407ef9162 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/magick/AbstractImageMagickContentTransformer.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform.magick; + +import java.io.File; +import java.io.InputStream; +import java.util.Collections; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.repo.content.transform.AbstractContentTransformer; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstract helper for transformations based on ImageMagick + * + * @author Derek Hulley + */ +public abstract class AbstractImageMagickContentTransformer extends AbstractContentTransformer +{ + /** the prefix for mimetypes supported by the transformer */ + public static final String MIMETYPE_IMAGE_PREFIX = "image/"; + + private static final Log logger = LogFactory.getLog(AbstractImageMagickContentTransformer.class); + + private MimetypeMap mimetypeMap; + private boolean available; + + public AbstractImageMagickContentTransformer() + { + this.available = false; + } + + /** + * Set the mimetype map to resolve mimetypes to file extensions. + * + * @param mimetypeMap + */ + public void setMimetypeMap(MimetypeMap mimetypeMap) + { + this.mimetypeMap = mimetypeMap; + } + + /** + * @return Returns true if the transformer is functioning otherwise false + */ + public boolean isAvailable() + { + return available; + } + + /** + * Make the transformer available + * @param available + */ + protected void setAvailable(boolean available) + { + this.available = available; + } + + /** + * Checks for the JMagick and ImageMagick dependencies, using the common + * {@link #transformInternal(File, File) transformation method} to check + * that the sample image can be converted. + */ + public void init() + { + if (mimetypeMap == null) + { + throw new AlfrescoRuntimeException("MimetypeMap not present"); + } + try + { + // load, into memory the sample gif + String resourcePath = "org/alfresco/repo/content/transform/magick/alfresco.gif"; + InputStream imageStream = getClass().getClassLoader().getResourceAsStream(resourcePath); + if (imageStream == null) + { + throw new AlfrescoRuntimeException("Sample image not found: " + resourcePath); + } + // dump to a temp file + File inputFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_init_source_", + ".gif"); + FileContentWriter writer = new FileContentWriter(inputFile); + writer.putContent(imageStream); + + // create the output file + File outputFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_init_target_", + ".png"); + + // execute it + Map options = Collections.emptyMap(); + transformInternal(inputFile, outputFile, options); + + // check that the file exists + if (!outputFile.exists()) + { + throw new Exception("Image conversion failed: \n" + + " from: " + inputFile + "\n" + + " to: " + outputFile); + } + // we can be sure that it works + setAvailable(true); + } + catch (Throwable e) + { + logger.error( + getClass().getSimpleName() + " not available: " + + (e.getMessage() != null ? e.getMessage() : "")); + // debug so that we can trace the issue if required + logger.debug(e); + } + } + + /** + * Some image formats are not supported by ImageMagick, or at least appear not to work. + * + * @param mimetype the mimetype to check + * @return Returns true if ImageMagic can handle the given image format + */ + public static boolean isSupported(String mimetype) + { + if (!mimetype.startsWith(MIMETYPE_IMAGE_PREFIX)) + { + return false; // not an image + } + else if (mimetype.equals(MimetypeMap.MIMETYPE_IMAGE_RGB)) + { + return false; // rgb extension doesn't work + } + else + { + return true; + } + } + + /** + * Supports image to image conversion, but only if the JMagick library and required + * libraries are available. + */ + public double getReliability(String sourceMimetype, String targetMimetype) + { + if (!available) + { + return 0.0; + } + if (!AbstractImageMagickContentTransformer.isSupported(sourceMimetype) || + !AbstractImageMagickContentTransformer.isSupported(targetMimetype)) + { + // only support IMAGE -> IMAGE (excl. RGB) + return 0.0; + } + else + { + return 1.0; + } + } + + /** + * @see #transformInternal(File, File) + */ + protected final void transformInternal( + ContentReader reader, + ContentWriter writer, + Map options) throws Exception + { + // get mimetypes + String sourceMimetype = getMimetype(reader); + String targetMimetype = getMimetype(writer); + + // get the extensions to use + String sourceExtension = mimetypeMap.getExtension(sourceMimetype); + String targetExtension = mimetypeMap.getExtension(targetMimetype); + if (sourceExtension == null || targetExtension == null) + { + throw new AlfrescoRuntimeException("Unknown extensions for mimetypes: \n" + + " source mimetype: " + sourceMimetype + "\n" + + " source extension: " + sourceExtension + "\n" + + " target mimetype: " + targetMimetype + "\n" + + " target extension: " + targetExtension); + } + + // if the source mimetype is the same as the target's then just stream it + if (sourceMimetype.equals(targetMimetype)) + { + writer.putContent(reader.getContentInputStream()); + return; + } + + // create required temp files + File sourceFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_source_", + "." + sourceExtension); + File targetFile = TempFileProvider.createTempFile( + getClass().getSimpleName() + "_target_", + "." + targetExtension); + + // pull reader file into source temp file + reader.getContent(sourceFile); + + // transform the source temp file to the target temp file + transformInternal(sourceFile, targetFile, options); + + // check that the file was created + if (!targetFile.exists()) + { + throw new ContentIOException("JMagick transformation failed to write output file"); + } + // upload the output image + writer.putContent(targetFile); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Transformation completed: \n" + + " source: " + reader + "\n" + + " target: " + writer + "\n" + + " options: " + options); + } + } + + /** + * Transform the image content from the source file to the target file + * + * @param sourceFile the source of the transformation + * @param targetFile the target of the transformation + * @param options the transformation options supported by ImageMagick + * @throws Exception + */ + protected abstract void transformInternal( + File sourceFile, + File targetFile, + Map options) throws Exception; +} diff --git a/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformer.java b/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformer.java new file mode 100644 index 0000000000..4e1b4a3e4b --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformer.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform.magick; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.util.exec.RuntimeExec; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Executes a statement to implement + * + * @author Derek Hulley + */ +public class ImageMagickContentTransformer extends AbstractImageMagickContentTransformer +{ + /** the command options, such as --resize, etc. */ + public static final String KEY_OPTIONS = "options"; + /** source variable name */ + public static final String VAR_OPTIONS = "options"; + /** source variable name */ + public static final String VAR_SOURCE = "source"; + /** target variable name */ + public static final String VAR_TARGET = "target"; + + private static final Log logger = LogFactory.getLog(ImageMagickContentTransformer.class); + + /** the system command executer */ + private RuntimeExec executer; + + public ImageMagickContentTransformer() + { + } + + /** + * Set the runtime command executer that must be executed in order to run + * ImageMagick. Whether or not this is the full path to the convertCommand + * or just the convertCommand itself depends the environment setup. + *

    + * The command must contain the variables ${source} and + * ${target}, which will be replaced by the names of the file to + * be transformed and the name of the output file respectively. + *

    +     *    convert ${source} ${target}
    +     * 
    + * + * @param executer the system command executer + */ + public void setExecuter(RuntimeExec executer) + { + this.executer = executer; + } + + /** + * Checks for the JMagick and ImageMagick dependencies, using the common + * {@link #transformInternal(File, File) transformation method} to check + * that the sample image can be converted. + */ + public void init() + { + if (executer == null) + { + throw new AlfrescoRuntimeException("System runtime executer not set"); + } + super.init(); + } + + /** + * Transform the image content from the source file to the target file + */ + protected void transformInternal(File sourceFile, File targetFile, Map options) throws Exception + { + Map properties = new HashMap(5); + // set properties + properties.put(KEY_OPTIONS, (String) options.get(KEY_OPTIONS)); + properties.put(VAR_SOURCE, sourceFile.getAbsolutePath()); + properties.put(VAR_TARGET, targetFile.getAbsolutePath()); + + // execute the statement + RuntimeExec.ExecutionResult result = executer.execute(properties); + if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0) + { + throw new ContentIOException("Failed to perform ImageMagick transformation: \n" + result); + } + // success + if (logger.isDebugEnabled()) + { + logger.debug("ImageMagic executed successfully: \n" + executer); + } + } +} diff --git a/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerTest.java new file mode 100644 index 0000000000..7d64dcbfec --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform.magick; + +import java.util.Collections; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.content.transform.ContentTransformer; +import org.alfresco.util.exec.RuntimeExec; + +/** + * @see org.alfresco.repo.content.transform.magick.JMagickContentTransformer + * + * @author Derek Hulley + */ +public class ImageMagickContentTransformerTest extends AbstractContentTransformerTest +{ + private ImageMagickContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + RuntimeExec executer = new RuntimeExec(); + executer.setCommand("imconvert.exe ${source} ${options} ${target}"); + executer.setDefaultProperties(Collections.singletonMap("options", "")); + + transformer = new ImageMagickContentTransformer(); + transformer.setMimetypeMap(mimetypeMap); + transformer.setExecuter(executer); + transformer.init(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + if (!transformer.isAvailable()) + { + return; + } + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_IMAGE_JPEG); + assertEquals("Mimetype should be supported", 1.0, reliability); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformer.java b/source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformer.java new file mode 100644 index 0000000000..13fbbd72e0 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform.magick; + +import java.io.File; +import java.util.Map; + +import magick.ImageInfo; +import magick.MagickImage; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Makes use of the {@link http://www.textmining.org/ TextMining} library to + * perform conversions from MSWord documents to text. + * + * @author Derek Hulley + */ +public class JMagickContentTransformer extends AbstractImageMagickContentTransformer +{ + private static final Log logger = LogFactory.getLog(JMagickContentTransformer.class); + + public JMagickContentTransformer() + { + } + + /** + * Uses the JMagick library to perform the transformation + * + * @param sourceFile + * @param targetFile + * @throws Exception + */ + @Override + protected void transformInternal(File sourceFile, File targetFile, Map options) throws Exception + { + ImageInfo imageInfo = new ImageInfo(sourceFile.getAbsolutePath()); + MagickImage image = new MagickImage(imageInfo); + image.setFileName(targetFile.getAbsolutePath()); + image.writeImage(imageInfo); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformerTest.java b/source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformerTest.java new file mode 100644 index 0000000000..dd386703d0 --- /dev/null +++ b/source/java/org/alfresco/repo/content/transform/magick/JMagickContentTransformerTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.content.transform.magick; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.content.transform.ContentTransformer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @see org.alfresco.repo.content.transform.magick.JMagickContentTransformer + * + * @author Derek Hulley + */ +public class JMagickContentTransformerTest extends AbstractContentTransformerTest +{ + private static final Log logger = LogFactory.getLog(JMagickContentTransformerTest.class); + + private JMagickContentTransformer transformer; + + public void onSetUpInTransaction() throws Exception + { + transformer = new JMagickContentTransformer(); + transformer.setMimetypeMap(mimetypeMap); + transformer.init(); + } + + /** + * @return Returns the same transformer regardless - it is allowed + */ + protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return transformer; + } + + public void testReliability() throws Exception + { + if (!transformer.isAvailable()) + { + return; + } + double reliability = 0.0; + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_TEXT_PLAIN); + assertEquals("Mimetype should not be supported", 0.0, reliability); + reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_IMAGE_JPEG); + assertEquals("Mimetype should be supported", 1.0, reliability); + } +} diff --git a/source/java/org/alfresco/repo/content/transform/magick/alfresco.gif b/source/java/org/alfresco/repo/content/transform/magick/alfresco.gif new file mode 100644 index 0000000000000000000000000000000000000000..0fe87596147526b7602dba2042357a562d0d012c GIT binary patch literal 4229 zcmb7@`9ISQ;J`n#*=CoIqasruS5oR3k)dPmBWiRwLUTps(VQvK%$cM)lC(6p@OVb9 z(9_7(a+WKFIZA{GU2322_xmS&-@m_qdcWRQHkMRF&o00S_znR67XtzUnSspUppdZ8 z@QCn;Yu6&9fG`+{gs(@ljQ=GVRmvRZlaBCZtn08QRq~cklx%Aik2fedHf}v$uX^gK zmRqx)+Y7*_L(-=mci|=a(o2GO8+h@hOh^wdydM+!M$%hw=)Ay0G6u%R!uVUTTq>-X z2rDPUTR8CcOnB#gScwNiw_(jRSd9y76vBIo;eDkrr35}u1{+nv(X1GYD%idjcBq3- zH^8oqu-7yAaxEO$1P8Xl@nb;ZTTJqMfHMiCP2kce3z|I;A{PZGuNw!l+FSR@NiDZvgdE;zRbzWW;H4#5R);QUdz{5@Pf4%dvqbt1TN z8tzzTcYTQ$e!11Xa=UjWt#38Ee>G!ZHK*lcS?8yxZlw1WGTEH2d~7ztC^y2b)t7%~1aH)|zIf5m+11(gQuwO7x3B-r zo57Kh(UH;7_wUCiCq;dq+lQBX$G-Hw_yG6J!aWP{&}Vpj6`uL_;^PnD!q4u-pF`q> zcJXpQ{7v}t#|ZrE?aa*V+^0{Ai;JH&W@p#n`3-pa2fViVVdM8_cw_eWpU+>ve*OOa z$J*xC^`2VBw6NLbPWkCD?)c<<|z>0x)QeEGs9#!gI6B4U&EURmD|Et`6%u}{+N{_0I4pyeS=5n28EtPwmE{*(K z`#Znqcc9+(#CyZ^v`ExNho#`mVwLMu_KC4nv07iN#@V&Cb77T5CgaA*VSToJD~Gr` z2K#2*y%A=L&u-*B{GBiqIuVs%ZG2{cV;b}Ag369yXVKTmxP$Y2+VX`j> zN~-)R#G`W0f1;F9qyG zZOQOe6;@=E9+^h5%`^wv)H!;ORseT`SS3HpKm4|Etd&|%F4^nz?`d3GCh$qeDnqy% zrCoyjGcZdj++MUt?*ZM^W>;UEI#F6T)PS3BICyQ`(1TWIvCS(6G|(m-6Z5>u-uMcn|lZ4ex0D`e5lUOw@^b0rJLt%FxvDT^Gx!wbX zqBtdpX!!?pBO%$b5lT=m2sWK0pX^#>DdcyrO_EjZR3-`!l+dKU>iG;~$;)}TDhxj?e|B4{2QWkxoGPoPv0`-Nm1;|`UuH)(KF_%srwgn za8e(Uaw48;ZA9{n#Y+f)_=p{gSWiHDOa$)97=Yp&2tF~>rg{pKcS!vd>AR4p29>_~ z-b&{{p^1`si6a5V(Vx3$7kaCyb?M|}0x~$1b2Jo3{K$xv?43#u0nx%w#RTnkDaD{z z2BOlLMU@dLNT$8ZjFZ{pt1wLp5<(`;G1a>yA%s&61T0co!qwFf=eg5l$T=|(i&7Mh z=0YALq#Xi2`mdy+JES?y%2GO&*jGlS1scApOh@WBfXZaOipvGyzTk-1DG5nqdjOkVW8DZ#z_} zMf##>5`ZShOPhjdb z)ERq79$q_%EUx`9kp+2Q(JuuNz&|Nr0k$ma@X^fQI`@8 z-vbzT>c-=zNqZ=DDja8R1QvbRUw@&lGdA=^YE)bfZMV=qJai?rl8hzaE#_ntJ4)MM z^f&oWrP(lLp)j;zUan;ihdVYO5oYIWVu>&->H^TZr7>RHUO8`1X@$yktqn_ql6NsS zc-fL{8JX$IFN(8{0HtlOfj%dc*A3J*`DYy3!|~C(NlF_MLr`tz$r+V45<=mv0SaIN zqgz#&Oa90w>yGSDv-?r%i>^LktdbjB{*6K+wRmr8khKAMR0FG>&W~HZR^MbzHT_ea z{8m-9&|7kM<@PmKu&ORLzt3AsnUH=Ipqx}$t-fr$u!)Jk{z8fF=)F^6S_@boao0GV z1i0+w{9s6;4OlM?OLcBq(qd#%8kh-r%y@l{8D!PeDyhpv5IEULasvx}gg&tq?^%wq zalO%Z$%GyM{IM?+S2Mr9kOu^jWhMyCSxo znvTnq3AJ}12|OfvB~52Zw35#gfGPPUhNRfI0=N~f?S&w+Sll`7IjI0a`-U0Yl;~(> zwyVwkz>Xy65gqNptgND7i_W>_eopGZKg!F&jyscO8rc=zbvEA~u6%q4hEiUlj}y+F zi|$W#a%OacNx;K7E<&XuU)hNwVO$2HJovFPL1${qoDL&ELSA!`wo6vB$)cB1lfuTs zF%O%)95V7}|M46~7eE1&oZpS18|rH17|1idF}iFPR6DnVIap>l0F;`U7ytcNg7v;G z2}Jv4h9EZFPE`Uox>5Gw{eqLR1k#StDYu`AQrmBe*z~ByL`Ttkf`?%u@c^4hUQLpQs)Zoxm>6S?k*}L+07PVS&Ol+NQCl`pT0K>F) zCEw698+9=KSItM>Kmnr;lCENFjJCwz2|?=eQSIeMNNXpfGL`2ms(hkKn0kU)@WR(A z-Lp_$9~B4?|0_IUFq<>k6SV8zDP)c49*;ITHF9MD;h;FAHNDhqm->;691Z-=MLLoweH;y7fAbN$^ zwk!H3{Vz=o*Bny~!@Fm2p4h7;;7~t8mVa4RfH12;Z(>-A0#ve<-*yNkz>rSR5uU?< zH0Y-aN?r2?mmQ-BX(SC4(Crnqgg#fKe+->=j(r#0um~s*Grx77U@vhIuj3e!u2+q5 zwr};p%HYk{5lSpCB@x1ef})8~OCsDkOFvUh=F@|I+6N`?7m&I^sZM*u%UQhMcj%wleh#(KBxKt z26wPjKV_P&c^t}`fih>ErtwlR4pxL=kYe!Hvi|LU%bVybM|)k&@+j?Xm&YQ-QHt(1 z&eG`Ca|HO&k^@AZFVbWe4FQ^lggFMZgVi(-V&eIR2Jz+XEY#u(_GibXec>*cT9<Pvwm*(G!pxbCmC4h;A?v>05^Dt6IW<1 ztx59`ZO4pJD-(M|WZ_Z@4zv8seTY2(>(@7gIiDDj&4ff*rZm*fa8P0}PeyrZ=A@(NPz&7{`Yl=@a22zel3&@Jpu>Dh71E_ zTH^PaTfrZ5LH?apYtCue=;;xkPhd3mlzkbBha7u!B#RXKooEvQexeiD0!$vcB^i7s%J~q6`T$97 zU=jE$^zR3xv~URRtPBegOUJbkkHG5*?=YWUyprWo+)qm7BtFi%Cpd#b`4>9pfKON` zblCYN2I5=djff815+t>S2jD3P0UC8|<+{tSVzdd#Nl@fmi&_+6Ndko0Le>c!iB3lx zrh1t2QI|_r@|o947yI##`#l%wBum~shx*eWDELx7+KGo7WRxa0K1S={ZCoFiGHkg< zWwLp=je>hL5anZAR`Cmb!a&Wt96i<_@sDdcQKiI&W_zy|RmAtJ`&Eu>Ar10yP6BiV R=dmECLfBZb1cHF^{{Rzqfb0MO literal 0 HcmV?d00001 diff --git a/source/java/org/alfresco/repo/copy/CopyServiceImpl.java b/source/java/org/alfresco/repo/copy/CopyServiceImpl.java new file mode 100644 index 0000000000..48e42308e0 --- /dev/null +++ b/source/java/org/alfresco/repo/copy/CopyServiceImpl.java @@ -0,0 +1,757 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.copy; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.CopyServiceException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.ParameterCheck; + +/** + * Node operations service implmentation. + * + * @author Roy Wetherall + */ +public class CopyServiceImpl implements CopyService +{ + /** + * The node service + */ + private NodeService nodeService; + + /** + * The dictionary service + */ + private DictionaryService dictionaryService; + + /** + * Policy component + */ + private PolicyComponent policyComponent; + + /** + * Rule service + */ + private RuleService ruleService; + + /** + * Policy delegates + */ + private ClassPolicyDelegate onCopyNodeDelegate; + private ClassPolicyDelegate onCopyCompleteDelegate; + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Sets the policy component + * + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the rule service + * + * @param ruleService the rule service + */ + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + /** + * Initialise method + */ + public void init() + { + // Register the policies + this.onCopyNodeDelegate = this.policyComponent.registerClassPolicy(CopyServicePolicies.OnCopyNodePolicy.class); + this.onCopyCompleteDelegate = this.policyComponent.registerClassPolicy(CopyServicePolicies.OnCopyCompletePolicy.class); + + // Register policy behaviours + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + ContentModel.ASPECT_COPIEDFROM, + new JavaBehaviour(this, "copyAspectOnCopy")); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"), + ContentModel.ASPECT_COPIEDFROM, + new JavaBehaviour(this, "onCopyComplete")); + } + + /** + * @see com.activiti.repo.node.copy.NodeCopyService#copy(com.activiti.repo.ref.NodeRef, com.activiti.repo.ref.NodeRef, com.activiti.repo.ref.QName, QName, boolean) + */ + public NodeRef copy( + NodeRef sourceNodeRef, + NodeRef destinationParent, + QName destinationAssocTypeQName, + QName destinationQName, + boolean copyChildren) + { + // Check that all the passed values are not null + ParameterCheck.mandatory("Source Node", sourceNodeRef); + ParameterCheck.mandatory("Destination Parent", destinationParent); + ParameterCheck.mandatory("Destination Association Name", destinationQName); + + if (sourceNodeRef.getStoreRef().equals(destinationParent.getStoreRef()) == false) + { + // TODO We need to create a new node in the other store with the same id as the source + + // Error - since at the moment we do not support cross store copying + throw new UnsupportedOperationException("Copying nodes across stores is not currently supported."); + } + + // Recursively copy node + Map copiedChildren = new HashMap(); + NodeRef copy = recursiveCopy(sourceNodeRef, destinationParent, destinationAssocTypeQName, destinationQName, copyChildren, copiedChildren); + + // Foreach of the newly created copies call the copy complete policy + for (Map.Entry entry : copiedChildren.entrySet()) + { + invokeCopyComplete(entry.getKey(), entry.getValue(), copiedChildren); + } + + return copy; + } + + /** + * Invokes the copy complete policy for the node reference provided + * + * @param sourceNodeRef the source node reference + * @param destinationNodeRef the destination node reference + * @param copiedNodeRefs the map of copied node references + */ + private void invokeCopyComplete( + NodeRef sourceNodeRef, + NodeRef destinationNodeRef, + Map copiedNodeRefs) + { + QName sourceClassRef = this.nodeService.getType(sourceNodeRef); + invokeCopyComplete(sourceClassRef, sourceNodeRef, destinationNodeRef, copiedNodeRefs); + + // Get the source aspects + Set sourceAspects = this.nodeService.getAspects(sourceNodeRef); + for (QName sourceAspect : sourceAspects) + { + invokeCopyComplete(sourceAspect, sourceNodeRef, destinationNodeRef, copiedNodeRefs); + } + } + + /** + * + * @param typeQName + * @param sourceNodeRef + * @param destinationNodeRef + * @param copiedNodeRefs + */ + private void invokeCopyComplete( + QName typeQName, + NodeRef sourceNodeRef, + NodeRef destinationNodeRef, + Map copiedNodeRefs) + { + Collection policies = this.onCopyCompleteDelegate.getList(typeQName); + if (policies.isEmpty() == true) + { + defaultOnCopyComplete(typeQName, sourceNodeRef, destinationNodeRef, copiedNodeRefs); + } + else + { + for (CopyServicePolicies.OnCopyCompletePolicy policy : policies) + { + policy.onCopyComplete(typeQName, sourceNodeRef, destinationNodeRef, copiedNodeRefs); + } + } + } + + /** + * + * @param typeQName + * @param sourceNodeRef + * @param destinationNodeRef + * @param copiedNodeRefs + */ + private void defaultOnCopyComplete( + QName typeQName, + NodeRef sourceNodeRef, + NodeRef destinationNodeRef, + Map copiedNodeRefs) + { + ClassDefinition classDefinition = this.dictionaryService.getClass(typeQName); + if (classDefinition != null) + { + // Check the properties + Map propertyDefinitions = classDefinition.getProperties(); + for (Map.Entry entry : propertyDefinitions.entrySet()) + { + QName propertyTypeDefinition = entry.getValue().getDataType().getName(); + if (DataTypeDefinition.NODE_REF.equals(propertyTypeDefinition) == true || + DataTypeDefinition.ANY.equals(propertyTypeDefinition) == true) + { + // Re-set the node ref so that it is still relative (if appropriate) + Serializable value = this.nodeService.getProperty(destinationNodeRef, entry.getKey()); + if (value != null && value instanceof NodeRef) + { + NodeRef nodeRef = (NodeRef)value; + if (copiedNodeRefs.containsKey(nodeRef) == true) + { + NodeRef copiedNodeRef = copiedNodeRefs.get(nodeRef); + this.nodeService.setProperty(destinationNodeRef, entry.getKey(), copiedNodeRef); + } + } + } + } + + // Copy the associations (child and target) + Map assocDefs = classDefinition.getAssociations(); + + // TODO: Need way of getting child assocs of a given type + List childAssocRefs = this.nodeService.getChildAssocs(destinationNodeRef); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + if (assocDefs.containsKey(childAssocRef.getTypeQName()) && + childAssocRef.isPrimary() == false && + copiedNodeRefs.containsKey(childAssocRef.getChildRef()) == true) + { + // Remove the assoc and re-point to the new node + this.nodeService.removeChild(destinationNodeRef, childAssocRef.getChildRef()); + this.nodeService.addChild( + destinationNodeRef, + copiedNodeRefs.get(childAssocRef.getChildRef()) , + childAssocRef.getTypeQName(), + childAssocRef.getQName()); + } + } + + // TODO: Need way of getting assocs of a given type + List nodeAssocRefs = this.nodeService.getTargetAssocs(destinationNodeRef, RegexQNamePattern.MATCH_ALL); + for (AssociationRef nodeAssocRef : nodeAssocRefs) + { + if (assocDefs.containsKey(nodeAssocRef.getTypeQName()) && + copiedNodeRefs.containsKey(nodeAssocRef.getTargetRef()) == true) + { + // Remove the assoc and re-point to the new node + this.nodeService.removeAssociation( + destinationNodeRef, + nodeAssocRef.getTargetRef(), + nodeAssocRef.getTypeQName()); + this.nodeService.createAssociation( + destinationNodeRef, + copiedNodeRefs.get(nodeAssocRef.getTargetRef()), + nodeAssocRef.getTypeQName()); + } + } + } + + } + + /** + * Recursive copy algorithm + * + * @param sourceNodeRef + * @param destinationParent + * @param destinationAssocTypeQName + * @param destinationQName + * @param copyChildren + * @param copiedChildren + * @return + */ + private NodeRef recursiveCopy( + NodeRef sourceNodeRef, + NodeRef destinationParent, + QName destinationAssocTypeQName, + QName destinationQName, + boolean copyChildren, + Map copiedChildren) + { + // Extract Type Definition + QName sourceTypeRef = this.nodeService.getType(sourceNodeRef); + TypeDefinition typeDef = dictionaryService.getType(sourceTypeRef); + if (typeDef == null) + { + throw new InvalidTypeException(sourceTypeRef); + } + + // Establish the scope of the copy + PolicyScope copyDetails = getCopyDetails(sourceNodeRef, destinationParent.getStoreRef(), true); + + // Create collection of properties for type and mandatory aspects + Map typeProps = copyDetails.getProperties(); + Map properties = new HashMap(); + if (typeProps != null) + { + properties.putAll(typeProps); + } + for (AspectDefinition aspectDef : typeDef.getDefaultAspects()) + { + Map aspectProps = copyDetails.getProperties(aspectDef.getName()); + if (aspectProps != null) + { + properties.putAll(aspectProps); + } + } + + // Create the new node + ChildAssociationRef destinationChildAssocRef = this.nodeService.createNode( + destinationParent, + destinationAssocTypeQName, + destinationQName, + sourceTypeRef, + properties); + NodeRef destinationNodeRef = destinationChildAssocRef.getChildRef(); + copiedChildren.put(sourceNodeRef, destinationNodeRef); + + // Prevent any rules being fired on the new destination node + this.ruleService.disableRules(destinationNodeRef); + try + { + // Apply the copy aspect to the new node + Map copyProperties = new HashMap(); + copyProperties.put(ContentModel.PROP_COPY_REFERENCE, sourceNodeRef); + this.nodeService.addAspect(destinationNodeRef, ContentModel.ASPECT_COPIEDFROM, copyProperties); + + // Copy the aspects + copyAspects(destinationNodeRef, copyDetails); + + // Copy the associations + copyAssociations(destinationNodeRef, copyDetails, copyChildren, copiedChildren); + } + finally + { + this.ruleService.enableRules(destinationNodeRef); + } + + return destinationNodeRef; + } + + /** + * Gets the copy details. This calls the appropriate policies that have been registered + * against the node and aspect types in order to pick-up any type specific copy behaviour. + *

    + * If no policies for a type are registered then the default copy takes place which will + * copy all properties and associations in the ususal manner. + * + * @param sourceNodeRef the source node reference + * @return the copy details + */ + private PolicyScope getCopyDetails(NodeRef sourceNodeRef, StoreRef destinationStoreRef, boolean copyToNewNode) + { + QName sourceClassRef = this.nodeService.getType(sourceNodeRef); + PolicyScope copyDetails = new PolicyScope(sourceClassRef); + + // Invoke the onCopy behaviour + invokeOnCopy(sourceClassRef, sourceNodeRef, destinationStoreRef, copyToNewNode, copyDetails); + + // TODO What do we do aboout props and assocs that are on the node node but not part of the type definition? + + // Get the source aspects + Set sourceAspects = this.nodeService.getAspects(sourceNodeRef); + for (QName sourceAspect : sourceAspects) + { + // Invoke the onCopy behaviour + invokeOnCopy(sourceAspect, sourceNodeRef, destinationStoreRef, copyToNewNode, copyDetails); + } + + return copyDetails; + } + + /** + * Invoke the correct onCopy behaviour + * + * @param sourceClassRef source class reference + * @param sourceNodeRef source node reference + * @param copyDetails the copy details + */ + private void invokeOnCopy( + QName sourceClassRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + Collection policies = this.onCopyNodeDelegate.getList(sourceClassRef); + if (policies.isEmpty() == true) + { + defaultOnCopy(sourceClassRef, sourceNodeRef, copyDetails); + } + else + { + for (CopyServicePolicies.OnCopyNodePolicy policy : policies) + { + policy.onCopyNode(sourceClassRef, sourceNodeRef, destinationStoreRef, copyToNewNode, copyDetails); + } + } + } + + /** + * Default implementation of on copy, used when there is no policy specified for a class. + * + * @param classRef the class reference of the node being copied + * @param sourceNodeRef the source node reference + * @param copyDetails details of the state being copied + */ + private void defaultOnCopy(QName classRef, NodeRef sourceNodeRef, PolicyScope copyDetails) + { + ClassDefinition classDefinition = this.dictionaryService.getClass(classRef); + if (classDefinition != null) + { + // Copy the properties + Map propertyDefinitions = classDefinition.getProperties(); + for (QName propertyName : propertyDefinitions.keySet()) + { + Serializable propValue = this.nodeService.getProperty(sourceNodeRef, propertyName); + copyDetails.addProperty(classDefinition.getName(), propertyName, propValue); + } + + // Copy the associations (child and target) + Map assocDefs = classDefinition.getAssociations(); + + // TODO: Need way of getting child assocs of a given type + if (classDefinition.isContainer()) + { + List childAssocRefs = this.nodeService.getChildAssocs(sourceNodeRef); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + if (assocDefs.containsKey(childAssocRef.getTypeQName())) + { + copyDetails.addChildAssociation(classDefinition.getName(), childAssocRef); + } + } + } + + // TODO: Need way of getting assocs of a given type + List nodeAssocRefs = this.nodeService.getTargetAssocs(sourceNodeRef, RegexQNamePattern.MATCH_ALL); + for (AssociationRef nodeAssocRef : nodeAssocRefs) + { + if (assocDefs.containsKey(nodeAssocRef.getTypeQName())) + { + copyDetails.addAssociation(classDefinition.getName(), nodeAssocRef); + } + } + } + } + + /** + * Copies the properties for the node type onto the destination node. + * + * @param destinationNodeRef the destintaion node reference + * @param copyDetails the copy details + */ + private void copyProperties(NodeRef destinationNodeRef, PolicyScope copyDetails) + { + Map props = copyDetails.getProperties(); + if (props != null) + { + for (QName propName : props.keySet()) + { + this.nodeService.setProperty(destinationNodeRef, propName, props.get(propName)); + } + } + } + + /** + * Applies the aspects (thus copying the associated properties) onto the destination node + * + * @param destinationNodeRef the destination node reference + * @param copyDetails the copy details + */ + private void copyAspects(NodeRef destinationNodeRef, PolicyScope copyDetails) + { + Set apects = copyDetails.getAspects(); + for (QName aspect : apects) + { + if (this.nodeService.hasAspect(destinationNodeRef, aspect) == false) + { + // Add the aspect to the node + this.nodeService.addAspect( + destinationNodeRef, + aspect, + copyDetails.getProperties(aspect)); + } + else + { + // Set each property on the destination node since the aspect has already been applied + Map aspectProps = copyDetails.getProperties(aspect); + if (aspectProps != null) + { + for (Map.Entry entry : aspectProps.entrySet()) + { + this.nodeService.setProperty(destinationNodeRef, entry.getKey(), entry.getValue()); + } + } + } + } + } + + /** + * Copies the associations (child and target) for the node type and aspects onto the + * destination node. + *

    + * If copyChildren is true then all child nodes of primary child associations are copied + * before they are associatied with the destination node. + * + * @param destinationNodeRef the destination node reference + * @param copyDetails the copy details + * @param copyChildren indicates whether the primary children are copied or not + * @param copiedChildren set of children already copied + */ + private void copyAssociations( + NodeRef destinationNodeRef, + PolicyScope copyDetails, + boolean copyChildren, + Map copiedChildren) + { + QName classRef = this.nodeService.getType(destinationNodeRef); + copyChildAssociations(classRef, destinationNodeRef, copyDetails, copyChildren, copiedChildren); + copyTargetAssociations(classRef, destinationNodeRef, copyDetails); + + Set apects = copyDetails.getAspects(); + for (QName aspect : apects) + { + if (this.nodeService.hasAspect(destinationNodeRef, aspect) == false) + { + // Error since the aspect has not been added to the destination node (should never happen) + throw new CopyServiceException("The aspect has not been added to the destination node."); + } + + copyChildAssociations(aspect, destinationNodeRef, copyDetails, copyChildren, copiedChildren); + copyTargetAssociations(aspect, destinationNodeRef, copyDetails); + } + } + + /** + * Copies the target associations onto the destination node reference. + * + * @param classRef the class reference + * @param destinationNodeRef the destination node reference + * @param copyDetails the copy details + */ + private void copyTargetAssociations(QName classRef, NodeRef destinationNodeRef, PolicyScope copyDetails) + { + List nodeAssocRefs = copyDetails.getAssociations(classRef); + if (nodeAssocRefs != null) + { + for (AssociationRef assocRef : nodeAssocRefs) + { + // Add the association + NodeRef targetRef = assocRef.getTargetRef(); + this.nodeService.createAssociation(destinationNodeRef, targetRef, assocRef.getTypeQName()); + } + } + } + + /** + * Copies the child associations onto the destiantion node reference. + *

    + * If copyChildren is true then the nodes at the end of a primary assoc will be copied before they + * are associated. + * + * @param classRef the class reference + * @param destinationNodeRef the destination node reference + * @param copyDetails the copy details + * @param copyChildren indicates whether to copy the primary children + */ + private void copyChildAssociations( + QName classRef, + NodeRef destinationNodeRef, + PolicyScope copyDetails, + boolean copyChildren, + Map copiedChildren) + { + List childAssocs = copyDetails.getChildAssociations(classRef); + if (childAssocs != null) + { + for (ChildAssociationRef childAssoc : childAssocs) + { + if (copyChildren == true) + { + if (childAssoc.isPrimary() == true) + { + // Do not recurse further, if we've already copied this node + if (copiedChildren.containsKey(childAssoc.getChildRef()) == false && + copiedChildren.containsValue(childAssoc.getChildRef()) == false) + { + // Copy the child + recursiveCopy( + childAssoc.getChildRef(), + destinationNodeRef, + childAssoc.getTypeQName(), + childAssoc.getQName(), + copyChildren, + copiedChildren); + } + } + else + { + // Add the child + NodeRef childRef = childAssoc.getChildRef(); + this.nodeService.addChild(destinationNodeRef, childRef, childAssoc.getTypeQName(), childAssoc.getQName()); + } + } + else + { + NodeRef childRef = childAssoc.getChildRef(); + QName childType = this.nodeService.getType(childRef); + + // TODO will need to remove this reference to the configurations association + if (this.dictionaryService.isSubClass(childType, ContentModel.TYPE_CONFIGURATIONS) == true || + copyDetails.isChildAssociationRefAlwaysTraversed(classRef, childAssoc) == true) + { + if (copiedChildren.containsKey(childRef) == false) + { + // Always recursivly copy configuration folders + recursiveCopy( + childRef, + destinationNodeRef, + childAssoc.getTypeQName(), + childAssoc.getQName(), + true, + copiedChildren); + } + } + else + { + // Add the child (will not be primary reguardless of its origional state) + this.nodeService.addChild(destinationNodeRef, childRef, childAssoc.getTypeQName(), childAssoc.getQName()); + } + } + } + } + } + + /** + * Defer to the standard implementation with copyChildren set to false + * + * @see com.activiti.repo.node.copy.NodeCopyService#copy(com.activiti.repo.ref.NodeRef, com.activiti.repo.ref.NodeRef, com.activiti.repo.ref.QName) + */ + public NodeRef copy( + NodeRef sourceNodeRef, + NodeRef destinationParent, + QName destinationAssocTypeQName, + QName destinationQName) + { + return copy( + sourceNodeRef, + destinationParent, + destinationAssocTypeQName, + destinationQName, + false); + } + + /** + * @see com.activiti.repo.node.copy.NodeCopyService#copy(com.activiti.repo.ref.NodeRef, com.activiti.repo.ref.NodeRef) + */ + public void copy( + NodeRef sourceNodeRef, + NodeRef destinationNodeRef) + { + // Check that the source and destination node are the same type + if (this.nodeService.getType(sourceNodeRef).equals(this.nodeService.getType(destinationNodeRef)) == false) + { + // Error - can not copy objects that are of different types + throw new CopyServiceException("The source and destination node must be the same type."); + } + + // Get the copy details + PolicyScope copyDetails = getCopyDetails(sourceNodeRef, destinationNodeRef.getStoreRef(), false); + + // Copy over the top of the destination node + copyProperties(destinationNodeRef, copyDetails); + copyAspects(destinationNodeRef, copyDetails); + copyAssociations(destinationNodeRef, copyDetails, false, new HashMap()); + } + + /** + * OnCopy behaviour registered for the copy aspect. + *

    + * Doing nothing in this behaviour ensures that the copy aspect found on the source node does not get + * copied onto the destination node. + * + * @param sourceClassRef the source class reference + * @param sourceNodeRef the source node reference + * @param copyDetails the copy details + */ + public void copyAspectOnCopy( + QName classRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + // Do nothing. This will ensure that copy aspect on the source node does not get copied onto + // the destination node. + } + + public void onCopyComplete( + QName classRef, + NodeRef sourceNodeRef, + NodeRef destinationRef, + Map copyMap) + { + // Do nothing since we do not want the copy from aspect to be relative to the copied nodes + } +} diff --git a/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java b/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java new file mode 100644 index 0000000000..593f7f1726 --- /dev/null +++ b/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java @@ -0,0 +1,707 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.copy; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.NoConditionEvaluator; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuter; +import org.alfresco.repo.action.executer.CopyActionExecuter; +import org.alfresco.repo.action.executer.MoveActionExecuter; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2Association; +import org.alfresco.repo.dictionary.M2ChildAssociation; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.dictionary.M2Type; +import org.alfresco.repo.rule.RuleModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.debug.NodeStoreInspector; + +/** + * Node operations service unit tests + * + * @author Roy Wetherall + */ +public class CopyServiceImplTest extends BaseSpringTest +{ + /** + * Services used by the tests + */ + private NodeService nodeService; + private CopyService copyService; + private DictionaryDAO dictionaryDAO; + private ContentService contentService; + private RuleService ruleService; + private ActionService actionService; + private AuthenticationComponent authenticationComponent; + + /** + * Data used by the tests + */ + private StoreRef storeRef; + private NodeRef sourceNodeRef; + private NodeRef rootNodeRef; + private NodeRef targetNodeRef; + private NodeRef nonPrimaryChildNodeRef; + private NodeRef childNodeRef; + private NodeRef destinationNodeRef; + + /** + * Types and properties used by the tests + */ + private static final String TEST_TYPE_NAMESPACE = "testTypeNamespaceURI"; + private static final QName TEST_TYPE_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testType"); + private static final QName PROP1_QNAME_MANDATORY = QName.createQName(TEST_TYPE_NAMESPACE, "prop1Mandatory"); + private static final QName PROP2_QNAME_OPTIONAL = QName.createQName(TEST_TYPE_NAMESPACE, "prop2Optional"); + + private static final QName TEST_ASPECT_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testAspect"); + private static final QName PROP3_QNAME_MANDATORY = QName.createQName(TEST_TYPE_NAMESPACE, "prop3Mandatory"); + private static final QName PROP4_QNAME_OPTIONAL = QName.createQName(TEST_TYPE_NAMESPACE, "prop4Optional"); + + private static final QName PROP_QNAME_MY_NODE_REF = QName.createQName(TEST_TYPE_NAMESPACE, "myNodeRef"); + private static final QName PROP_QNAME_MY_ANY = QName.createQName(TEST_TYPE_NAMESPACE, "myAny"); + + private static final QName TEST_MANDATORY_ASPECT_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testMandatoryAspect"); + private static final QName PROP5_QNAME_MANDATORY = QName.createQName(TEST_TYPE_NAMESPACE, "prop5Mandatory"); + + private static final String TEST_VALUE_1 = "testValue1"; + private static final String TEST_VALUE_2 = "testValue2"; + private static final String TEST_VALUE_3 = "testValue3"; + + private static final QName TEST_CHILD_ASSOC_TYPE_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "contains"); + private static final QName TEST_CHILD_ASSOC_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testChildAssocName"); + private static final QName TEST_ASSOC_TYPE_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testAssocName"); + private static final QName TEST_CHILD_ASSOC_QNAME2 = QName.createQName(TEST_TYPE_NAMESPACE, "testChildAssocName2"); + + private static final ContentData CONTENT_DATA_TEXT = new ContentData(null, "text/plain", 0L, "UTF-8"); + + /** + * Test content + */ + private static final String SOME_CONTENT = "This is some content ..."; + + /** + * Sets the meta model DAO + * + * @param dictionaryDAO the meta model DAO + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + /** + * On setup in transaction implementation + */ + @Override + protected void onSetUpInTransaction() + throws Exception + { + // Set the services + this.nodeService = (NodeService)this.applicationContext.getBean("dbNodeService"); + this.copyService = (CopyService)this.applicationContext.getBean("copyService"); + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + this.ruleService = (RuleService)this.applicationContext.getBean("ruleService"); + this.actionService = (ActionService)this.applicationContext.getBean("actionService"); + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + // Create the test model + createTestModel(); + + // Create the store and get the root node reference + this.storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(storeRef); + + // Create the node used for copying + ChildAssociationRef childAssocRef = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}test"), + TEST_TYPE_QNAME, + createTypePropertyBag()); + this.sourceNodeRef = childAssocRef.getChildRef(); + + // Create another bag of properties + Map aspectProperties = new HashMap(); + aspectProperties.put(PROP3_QNAME_MANDATORY, TEST_VALUE_1); + aspectProperties.put(PROP4_QNAME_OPTIONAL, TEST_VALUE_2); + + // Apply the test aspect + this.nodeService.addAspect( + this.sourceNodeRef, + TEST_ASPECT_QNAME, + aspectProperties); + + this.nodeService.addAspect(sourceNodeRef, ContentModel.ASPECT_TITLED, null); + + // Add a child + ChildAssociationRef temp3 =this.nodeService.createNode( + this.sourceNodeRef, + TEST_CHILD_ASSOC_TYPE_QNAME, + TEST_CHILD_ASSOC_QNAME, + TEST_TYPE_QNAME, + createTypePropertyBag()); + this.childNodeRef = temp3.getChildRef(); + + // Add a child that is primary + ChildAssociationRef temp2 = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testNonPrimaryChild"), + TEST_TYPE_QNAME, + createTypePropertyBag()); + + this.nonPrimaryChildNodeRef = temp2.getChildRef(); + this.nodeService.addChild( + this.sourceNodeRef, + this.nonPrimaryChildNodeRef, + TEST_CHILD_ASSOC_TYPE_QNAME, + TEST_CHILD_ASSOC_QNAME2); + + // Add a target assoc + ChildAssociationRef temp = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testAssoc"), + TEST_TYPE_QNAME, + createTypePropertyBag()); + this.targetNodeRef = temp.getChildRef(); + this.nodeService.createAssociation(this.sourceNodeRef, this.targetNodeRef, TEST_ASSOC_TYPE_QNAME); + + // Create a node we can use as the destination in a copy + Map destinationProps = new HashMap(); + destinationProps.put(PROP1_QNAME_MANDATORY, TEST_VALUE_1); + destinationProps.put(PROP5_QNAME_MANDATORY, TEST_VALUE_3); + destinationProps.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + ChildAssociationRef temp5 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testDestinationNode"), + TEST_TYPE_QNAME, + destinationProps); + this.destinationNodeRef = temp5.getChildRef(); + } + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + /** + * Helper method that creates a bag of properties for the test type + * + * @return bag of properties + */ + private Map createTypePropertyBag() + { + Map result = new HashMap(); + result.put(PROP1_QNAME_MANDATORY, TEST_VALUE_1); + result.put(PROP2_QNAME_OPTIONAL, TEST_VALUE_2); + result.put(PROP5_QNAME_MANDATORY, TEST_VALUE_3); + result.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + return result; + } + + /** + * Creates the test model used by the tests + */ + private void createTestModel() + { + M2Model model = M2Model.createModel("test:nodeoperations"); + model.createNamespace(TEST_TYPE_NAMESPACE, "test"); + model.createImport(NamespaceService.DICTIONARY_MODEL_1_0_URI, NamespaceService.DICTIONARY_MODEL_PREFIX); + model.createImport(NamespaceService.SYSTEM_MODEL_1_0_URI, NamespaceService.SYSTEM_MODEL_PREFIX); + model.createImport(NamespaceService.CONTENT_MODEL_1_0_URI, NamespaceService.CONTENT_MODEL_PREFIX); + + M2Type testType = model.createType("test:" + TEST_TYPE_QNAME.getLocalName()); + testType.setParentName("cm:" + ContentModel.TYPE_CONTENT.getLocalName()); + + M2Property prop1 = testType.createProperty("test:" + PROP1_QNAME_MANDATORY.getLocalName()); + prop1.setMandatory(true); + prop1.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop1.setMultiValued(false); + + M2Property prop2 = testType.createProperty("test:" + PROP2_QNAME_OPTIONAL.getLocalName()); + prop2.setMandatory(false); + prop2.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop2.setMandatory(false); + + M2Property propNodeRef = testType.createProperty("test:" + PROP_QNAME_MY_NODE_REF.getLocalName()); + propNodeRef.setMandatory(false); + propNodeRef.setType("d:" + DataTypeDefinition.NODE_REF.getLocalName()); + propNodeRef.setMandatory(false); + + M2Property propAnyNodeRef = testType.createProperty("test:" + PROP_QNAME_MY_ANY.getLocalName()); + propAnyNodeRef.setMandatory(false); + propAnyNodeRef.setType("d:" + DataTypeDefinition.ANY.getLocalName()); + propAnyNodeRef.setMandatory(false); + + M2ChildAssociation childAssoc = testType.createChildAssociation("test:" + TEST_CHILD_ASSOC_TYPE_QNAME.getLocalName()); + childAssoc.setTargetClassName("sys:base"); + childAssoc.setTargetMandatory(false); + + M2Association assoc = testType.createAssociation("test:" + TEST_ASSOC_TYPE_QNAME.getLocalName()); + assoc.setTargetClassName("sys:base"); + assoc.setTargetMandatory(false); + + M2Aspect testAspect = model.createAspect("test:" + TEST_ASPECT_QNAME.getLocalName()); + + M2Property prop3 = testAspect.createProperty("test:" + PROP3_QNAME_MANDATORY.getLocalName()); + prop3.setMandatory(true); + prop3.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop3.setMultiValued(false); + + M2Property prop4 = testAspect.createProperty("test:" + PROP4_QNAME_OPTIONAL.getLocalName()); + prop4.setMandatory(false); + prop4.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop4.setMultiValued(false); + + M2Aspect testMandatoryAspect = model.createAspect("test:" + TEST_MANDATORY_ASPECT_QNAME.getLocalName()); + M2Property prop5 = testMandatoryAspect.createProperty("test:" + PROP5_QNAME_MANDATORY.getLocalName()); + prop5.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop5.setMandatory(true); + + testType.addMandatoryAspect("test:" + TEST_MANDATORY_ASPECT_QNAME.getLocalName()); + + dictionaryDAO.putModel(model); + } + + /** + * Test copy new node within store + */ + public void testCopyToNewNode() + { + // Copy to new node without copying children + NodeRef copy = this.copyService.copy( + this.sourceNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}copyAssoc")); + checkCopiedNode(this.sourceNodeRef, copy, true, true, false); + + // Copy to new node, copying children + NodeRef copy2 = this.copyService.copy( + this.sourceNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}copyAssoc"), + true); + checkCopiedNode(this.sourceNodeRef, copy2, true, true, true); + + // Check that a copy of a copy works correctly + NodeRef copyOfCopy = this.copyService.copy( + copy, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}copyOfCopy")); + checkCopiedNode(copy, copyOfCopy, true, true, false); + + // TODO check copying from a versioned copy + // TODO check copying from a lockable copy + + // Check copying from a node with content + ContentWriter contentWriter = this.contentService.getWriter(this.sourceNodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.putContent(SOME_CONTENT); + NodeRef copyWithContent = this.copyService.copy( + this.sourceNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}copyWithContent")); + checkCopiedNode(this.sourceNodeRef, copyWithContent, true, true, false); + ContentReader contentReader = this.contentService.getReader(copyWithContent, ContentModel.PROP_CONTENT); + assertNotNull(contentReader); + assertEquals(SOME_CONTENT, contentReader.getContentString()); + + // TODO check copying to a different store + + //System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + } + + public void testCopyNodeWithRules() + { + // Create a new rule and add it to the source noderef + Rule rule = this.ruleService.createRule(RuleType.INBOUND); + + Map props = new HashMap(1); + props.put(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); + Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME, props); + rule.addAction(action); + + ActionCondition actionCondition = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + rule.addActionCondition(actionCondition); + + this.ruleService.saveRule(this.sourceNodeRef, rule); + + //System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + //System.out.println(" ------------------------------ "); + + // Now copy the node that has rules associated with it + NodeRef copy = this.copyService.copy( + this.sourceNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}withRulesCopy"), + true); + + //System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + + checkCopiedNode(this.sourceNodeRef, copy, true, true, true); + + //assertTrue(this.configurableService.isConfigurable(copy)); + //assertNotNull(this.configurableService.getConfigurationFolder(copy)); + //assertFalse(this.configurableService.getConfigurationFolder(this.sourceNodeRef) == this.configurableService.getConfigurationFolder(copy)); + + assertTrue(this.nodeService.hasAspect(copy, RuleModel.ASPECT_RULES)); + assertTrue(this.ruleService.hasRules(copy)); + assertTrue(this.ruleService.rulesEnabled(copy)); + List copiedRules = this.ruleService.getRules(copy); + assertEquals(1, copiedRules.size()); + Rule copiedRule = copiedRules.get(0); + assertFalse(rule.getId() == copiedRule.getId()); + assertEquals(rule.getAction(0).getActionDefinitionName(), copiedRule.getAction(0).getActionDefinitionName()); + + // Now copy the node without copying the children and check that the rules have been copied + NodeRef copy2 = this.copyService.copy( + this.sourceNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}withRuleCopyNoChildren"), + false); + +// System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + + checkCopiedNode(this.sourceNodeRef, copy2, true, true, false); + + //assertTrue(this.configurableService.isConfigurable(copy2)); + //assertNotNull(this.configurableService.getConfigurationFolder(copy2)); + //assertFalse(this.configurableService.getConfigurationFolder(this.sourceNodeRef) == this.configurableService.getConfigurationFolder(copy2)); + + assertTrue(this.nodeService.hasAspect(copy2, RuleModel.ASPECT_RULES)); + assertTrue(this.ruleService.hasRules(copy2)); + assertTrue(this.ruleService.rulesEnabled(copy2)); + List copiedRules2 = this.ruleService.getRules(copy2); + assertEquals(1, copiedRules.size()); + Rule copiedRule2 = copiedRules2.get(0); + assertFalse(rule.getId() == copiedRule2.getId()); + assertEquals(rule.getAction(0).getActionDefinitionName(), copiedRule2.getAction(0).getActionDefinitionName()); + } + + public void testCopyToExistingNode() + { + // Copy nodes within the same store + this.copyService.copy(this.sourceNodeRef, this.destinationNodeRef); + checkCopiedNode(this.sourceNodeRef, this.destinationNodeRef, false, true, false); + + // TODO check copying from a copy + // TODO check copying from a versioned copy + // TODO check copying from a lockable copy + // TODO check copying from a node with content + + // TODO check copying nodes between stores + + //System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + } + + /** + * Test a potentially recursive copy + */ + public void testRecursiveCopy() + { + // Need to create a potentially recursive node structure + NodeRef nodeOne = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef nodeTwo = this.nodeService.createNode( + nodeOne, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef nodeThree = this.nodeService.createNode( + nodeTwo, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + // Issue a potentialy recursive copy + this.copyService.copy(nodeOne, nodeThree, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, true); + + //System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + } + + /** + * Test that realtive links between nodes are restored once the copy is completed + */ + public void testRelativeLinks() + { + QName nodeOneAssocName = QName.createQName("{test}nodeOne"); + QName nodeTwoAssocName = QName.createQName("{test}nodeTwo"); + QName nodeThreeAssocName = QName.createQName("{test}nodeThree"); + QName nodeFourAssocName = QName.createQName("{test}nodeFour"); + + NodeRef nodeOne = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + nodeOneAssocName, + TEST_TYPE_QNAME).getChildRef(); + NodeRef nodeTwo = this.nodeService.createNode( + nodeOne, + TEST_CHILD_ASSOC_TYPE_QNAME, + nodeTwoAssocName, + TEST_TYPE_QNAME).getChildRef(); + NodeRef nodeThree = this.nodeService.createNode( + nodeTwo, + TEST_CHILD_ASSOC_TYPE_QNAME, + nodeThreeAssocName, + TEST_TYPE_QNAME).getChildRef(); + NodeRef nodeFour = this.nodeService.createNode( + nodeOne, + TEST_CHILD_ASSOC_TYPE_QNAME, + nodeFourAssocName, + TEST_TYPE_QNAME).getChildRef(); + this.nodeService.addChild(nodeFour, nodeThree, TEST_CHILD_ASSOC_TYPE_QNAME, TEST_CHILD_ASSOC_QNAME); + this.nodeService.createAssociation(nodeTwo, nodeThree, TEST_ASSOC_TYPE_QNAME); + this.nodeService.setProperty(nodeOne, PROP_QNAME_MY_NODE_REF, nodeThree); + this.nodeService.setProperty(nodeOne, PROP_QNAME_MY_ANY, nodeThree); + + // Make node one actionable with a rule to copy nodes into node two + Map params = new HashMap(1); + params.put(MoveActionExecuter.PARAM_DESTINATION_FOLDER, nodeTwo); + params.put(MoveActionExecuter.PARAM_ASSOC_TYPE_QNAME, TEST_CHILD_ASSOC_TYPE_QNAME); + params.put(MoveActionExecuter.PARAM_ASSOC_QNAME, QName.createQName("{test}ruleCopy")); + Rule rule = this.ruleService.createRule(RuleType.INBOUND); + ActionCondition condition = this.actionService.createActionCondition(NoConditionEvaluator.NAME); + rule.addActionCondition(condition); + Action action = this.actionService.createAction(CopyActionExecuter.NAME, params); + rule.addAction(action); + this.ruleService.saveRule(nodeOne, rule); + + // Do a deep copy + NodeRef nodeOneCopy = this.copyService.copy(nodeOne, this.rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{test}copiedNodeOne"), true); + NodeRef nodeTwoCopy = null; + NodeRef nodeThreeCopy = null; + NodeRef nodeFourCopy = null; + + //System.out.println( + // NodeStoreInspector.dumpNodeStore(this.nodeService, this.storeRef)); + + List nodeOneCopyChildren = this.nodeService.getChildAssocs(nodeOneCopy); + assertNotNull(nodeOneCopyChildren); + assertEquals(3, nodeOneCopyChildren.size()); + for (ChildAssociationRef nodeOneCopyChild : nodeOneCopyChildren) + { + if (nodeOneCopyChild.getQName().equals(nodeTwoAssocName) == true) + { + nodeTwoCopy = nodeOneCopyChild.getChildRef(); + + List nodeTwoCopyChildren = this.nodeService.getChildAssocs(nodeTwoCopy); + assertNotNull(nodeTwoCopyChildren); + assertEquals(1, nodeTwoCopyChildren.size()); + for (ChildAssociationRef nodeTwoCopyChild : nodeTwoCopyChildren) + { + if (nodeTwoCopyChild.getQName().equals(nodeThreeAssocName) == true) + { + nodeThreeCopy = nodeTwoCopyChild.getChildRef(); + } + } + } + else if (nodeOneCopyChild.getQName().equals(nodeFourAssocName) == true) + { + nodeFourCopy = nodeOneCopyChild.getChildRef(); + } + } + assertNotNull(nodeTwoCopy); + assertNotNull(nodeThreeCopy); + assertNotNull(nodeFourCopy); + + // Check the non primary child assoc + List children = this.nodeService.getChildAssocs( + nodeFourCopy, + RegexQNamePattern.MATCH_ALL, + TEST_CHILD_ASSOC_QNAME); + assertNotNull(children); + assertEquals(1, children.size()); + ChildAssociationRef child = children.get(0); + assertEquals(child.getChildRef(), nodeThreeCopy); + + // Check the node ref property + NodeRef nodeRef = (NodeRef)this.nodeService.getProperty(nodeOneCopy, PROP_QNAME_MY_NODE_REF); + assertNotNull(nodeRef); + assertEquals(nodeThreeCopy, nodeRef); + + // Check the any property + NodeRef anyNodeRef = (NodeRef)this.nodeService.getProperty(nodeOneCopy, PROP_QNAME_MY_ANY); + assertNotNull(anyNodeRef); + assertEquals(nodeThreeCopy, anyNodeRef); + + // Check the target assoc + List assocs = this.nodeService.getTargetAssocs(nodeTwoCopy, TEST_ASSOC_TYPE_QNAME); + assertNotNull(assocs); + assertEquals(1, assocs.size()); + AssociationRef assoc = assocs.get(0); + assertEquals(assoc.getTargetRef(), nodeThreeCopy); + + // Check that the rule parameter values have been made relative + List rules = this.ruleService.getRules(nodeOneCopy); + assertNotNull(rules); + assertEquals(1, rules.size()); + Rule copiedRule = rules.get(0); + assertNotNull(copiedRule); + List ruleActions = copiedRule.getActions(); + assertNotNull(ruleActions); + assertEquals(1, ruleActions.size()); + Action ruleAction = ruleActions.get(0); + NodeRef value = (NodeRef)ruleAction.getParameterValue(MoveActionExecuter.PARAM_DESTINATION_FOLDER); + assertNotNull(value); + assertEquals(nodeTwoCopy, value); + } + + /** + * Check that the copied node contains the state we are expecting + * + * @param sourceNodeRef the source node reference + * @param destinationNodeRef the destination node reference + */ + private void checkCopiedNode(NodeRef sourceNodeRef, NodeRef destinationNodeRef, boolean newCopy, boolean sameStore, boolean copyChildren) + { + if (newCopy == true) + { + if (sameStore == true) + { + // Check that the copy aspect has been applied to the copy + boolean hasCopyAspect = this.nodeService.hasAspect(destinationNodeRef, ContentModel.ASPECT_COPIEDFROM); + assertTrue(hasCopyAspect); + NodeRef copyNodeRef = (NodeRef)this.nodeService.getProperty(destinationNodeRef, ContentModel.PROP_COPY_REFERENCE); + assertNotNull(copyNodeRef); + assertEquals(sourceNodeRef, copyNodeRef); + } + else + { + // Check that destiantion has the same id as the source + assertEquals(sourceNodeRef.getId(), destinationNodeRef.getId()); + } + } + + boolean hasTestAspect = this.nodeService.hasAspect(destinationNodeRef, TEST_ASPECT_QNAME); + assertTrue(hasTestAspect); + + // Check that all the correct properties have been copied + Map destinationProperties = this.nodeService.getProperties(destinationNodeRef); + assertNotNull(destinationProperties); + String value1 = (String)destinationProperties.get(PROP1_QNAME_MANDATORY); + assertNotNull(value1); + assertEquals(TEST_VALUE_1, value1); + String value2 = (String)destinationProperties.get(PROP2_QNAME_OPTIONAL); + assertNotNull(value2); + assertEquals(TEST_VALUE_2, value2); + String value3 = (String)destinationProperties.get(PROP3_QNAME_MANDATORY); + assertNotNull(value3); + assertEquals(TEST_VALUE_1, value3); + String value4 = (String)destinationProperties.get(PROP4_QNAME_OPTIONAL); + assertNotNull(value4); + assertEquals(TEST_VALUE_2, value4); + + // Check all the target associations have been copied + List destinationTargets = this.nodeService.getTargetAssocs(destinationNodeRef, TEST_ASSOC_TYPE_QNAME); + assertNotNull(destinationTargets); + assertEquals(1, destinationTargets.size()); + AssociationRef nodeAssocRef = destinationTargets.get(0); + assertNotNull(nodeAssocRef); + assertEquals(this.targetNodeRef, nodeAssocRef.getTargetRef()); + + // Check all the child associations have been copied + List childAssocRefs = this.nodeService.getChildAssocs(destinationNodeRef); + assertNotNull(childAssocRefs); + int expectedSize = 2; + if (this.nodeService.hasAspect(destinationNodeRef, RuleModel.ASPECT_RULES) == true) + { + expectedSize = expectedSize + 1; + } + + assertEquals(expectedSize, childAssocRefs.size()); + for (ChildAssociationRef ref : childAssocRefs) + { + if (ref.getQName().equals(TEST_CHILD_ASSOC_QNAME2) == true) + { + // Since this child is non-primary in the source it will always be non-primary in the destination + assertFalse(ref.isPrimary()); + assertEquals(this.nonPrimaryChildNodeRef, ref.getChildRef()); + } + else + { + if (copyChildren == false) + { + if (ref.getTypeQName().equals(RuleModel.ASSOC_RULE_FOLDER) == true) + { + assertTrue(ref.isPrimary()); + assertTrue(this.childNodeRef.equals(ref.getChildRef()) == false); + } + else + { + assertFalse(ref.isPrimary()); + assertEquals(this.childNodeRef, ref.getChildRef()); + } + } + else + { + assertTrue(ref.isPrimary()); + assertTrue(this.childNodeRef.equals(ref.getChildRef()) == false); + + // TODO need to check that the copied child has all the correct details .. + } + } + } + } +} diff --git a/source/java/org/alfresco/repo/copy/CopyServicePolicies.java b/source/java/org/alfresco/repo/copy/CopyServicePolicies.java new file mode 100644 index 0000000000..d68f154fc2 --- /dev/null +++ b/source/java/org/alfresco/repo/copy/CopyServicePolicies.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.copy; + +import java.util.Map; + +import org.alfresco.repo.policy.ClassPolicy; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + +/** + * @author Roy Wetherall + */ +public interface CopyServicePolicies +{ + /** + * Policy invoked when a node is copied + */ + public interface OnCopyNodePolicy extends ClassPolicy + { + /** + * @param classRef the type of node being copied + * @param sourceNodeRef node being copied + * @param destinationStoreRef the destination store reference + * @param copyToNewNode indicates whether we are copying to a new node or not + * @param copyDetails modifiable node details + */ + public void onCopyNode( + QName classRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails); + } + + /** + * Policy invoked when the copy operation invoked on a node is complete. + *

    + * The copy map contains all the nodes created during the copy, this helps to re-map + * any potentially relative associations. + */ + public interface OnCopyCompletePolicy extends ClassPolicy + { + /** + * @param classRef the type of the node that was copied + * @param sourceNodeRef the origional node + * @param destinationRef the destination node + * @param copyMap a map containing all the nodes that have been created during the copy + */ + public void onCopyComplete( + QName classRef, + NodeRef sourceNodeRef, + NodeRef destinationRef, + Map copyMap); + } +} diff --git a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java new file mode 100644 index 0000000000..18eb9f3291 --- /dev/null +++ b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.descriptor; + +import java.io.IOException; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.alfresco.repo.importer.ImporterBootstrap; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.repository.InvalidStoreRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.io.Resource; + + +/** + * Implementation of Descriptor Service + * + * @author David Caruana + */ +public class DescriptorServiceImpl implements DescriptorService, ApplicationListener +{ + private Properties serverProperties; + + private ImporterBootstrap systemBootstrap; + private NamespaceService namespaceService; + private NodeService nodeService; + private SearchService searchService; + private TransactionService transactionService; + + private Descriptor serverDescriptor; + private Descriptor repoDescriptor; + + + // Logger + private static final Log logger = LogFactory.getLog(DescriptorService.class); + + + /** + * Sets the server descriptor from a resource file + * + * @param descriptorResource resource containing server descriptor meta-data + * @throws IOException + */ + public void setServerDescriptor(Resource descriptorResource) + throws IOException + { + this.serverProperties = new Properties(); + this.serverProperties.load(descriptorResource.getInputStream()); + } + + /** + * @param systemBootstrap system bootstrap + */ + public void setSystemBootstrap(ImporterBootstrap systemBootstrap) + { + this.systemBootstrap = systemBootstrap; + } + + /** + * @param transactionService transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param searchService search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.DescriptorService#getDescriptor() + */ + public Descriptor getDescriptor() + { + return serverDescriptor; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.DescriptorService#getRepositoryDescriptor() + */ + public Descriptor getRepositoryDescriptor() + { + return repoDescriptor; + } + + public void init() + { + // initialise descriptors + serverDescriptor = createServerDescriptor(); + repoDescriptor = TransactionUtil.executeInUserTransaction(transactionService, new TransactionUtil.TransactionWork() + { + public Descriptor doWork() + { + return createRepositoryDescriptor(); + } + }); + } + + /** + * @param event + */ + public void onApplicationEvent(ApplicationEvent event) + { + if (event instanceof ContextRefreshedEvent) + { + if (serverDescriptor != null) + { + // log output of version initialised + String serverVersion = serverDescriptor.getVersion(); + String serverEdition = serverDescriptor.getEdition(); + String repoVersion = repoDescriptor.getVersion(); + + if (logger.isInfoEnabled()) + logger.info("Alfresco started (" + serverEdition + ") - v" + serverVersion + "; repository v" + repoVersion); + } + } + } + + /** + * Create server descriptor + * + * @return descriptor + */ + private Descriptor createServerDescriptor() + { + return new ServerDescriptor(); + } + + /** + * Create repository descriptor + * + * @return descriptor + */ + private Descriptor createRepositoryDescriptor() + { + // retrieve system descriptor location + StoreRef storeRef = systemBootstrap.getStoreRef(); + Properties systemProperties = systemBootstrap.getConfiguration(); + String path = systemProperties.getProperty("system.descriptor.childname"); + + // retrieve system descriptor + NodeRef descriptorRef = null; + try + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + List nodeRefs = searchService.selectNodes(rootNode, "/" + path, null, namespaceService, false); + if (nodeRefs.size() > 0) + { + descriptorRef = nodeRefs.get(0); + } + } + catch(InvalidStoreRefException e) + { + // handle as system descriptor not found + } + + // create appropriate descriptor + if (descriptorRef != null) + { + Map properties = nodeService.getProperties(descriptorRef); + return new RepositoryDescriptor(properties); + } + + // descriptor cannot be found + return new UnknownDescriptor(); + } + + /** + * Unknown descriptor + * + * @author David Caruana + */ + private class UnknownDescriptor implements Descriptor + { + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionMajor() + */ + public String getVersionMajor() + { + return "unknown"; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionMinor() + */ + public String getVersionMinor() + { + return "unknown"; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionRevision() + */ + public String getVersionRevision() + { + return "unknown"; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionLabel() + */ + public String getVersionLabel() + { + return "unknown"; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersion() + */ + public String getVersion() + { + return "unknown (pre 1.0.0 RC2)"; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getEdition() + */ + public String getEdition() + { + return "unknown"; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getDescriptorKeys() + */ + public String[] getDescriptorKeys() + { + return new String[0]; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getDescriptor(java.lang.String) + */ + public String getDescriptor(String key) + { + return null; + } + } + + /** + * Repository Descriptor whose meta-data is retrieved from the repository store + */ + private class RepositoryDescriptor implements Descriptor + { + private Map properties; + + + /** + * Construct + * + * @param properties system descriptor properties + */ + private RepositoryDescriptor(Map properties) + { + this.properties = properties; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionMajor() + */ + public String getVersionMajor() + { + return getDescriptor("sys:versionMajor"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionMinor() + */ + public String getVersionMinor() + { + return getDescriptor("sys:versionMinor"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionRevision() + */ + public String getVersionRevision() + { + return getDescriptor("sys:versionRevision"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionLabel() + */ + public String getVersionLabel() + { + return getDescriptor("sys:versionLabel"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersion() + */ + public String getVersion() + { + String version = getVersionMajor() + "." + getVersionMinor() + "." + getVersionRevision(); + String label = getVersionLabel(); + if (label != null && label.length() > 0) + { + version += " (" + label + ")"; + } + return version; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getEdition() + */ + public String getEdition() + { + return null; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getDescriptorKeys() + */ + public String[] getDescriptorKeys() + { + String[] keys = new String[properties.size()]; + properties.keySet().toArray(keys); + return keys; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getDescriptor(java.lang.String) + */ + public String getDescriptor(String key) + { + String strValue = null; + QName qname = QName.createQName(key, namespaceService); + Serializable value = properties.get(qname); + if (value != null) + { + strValue = value.toString(); + } + return strValue; + } + } + + /** + * Server Descriptor whose meta-data is retrieved from run-time environment + */ + private class ServerDescriptor implements Descriptor + { + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionMajor() + */ + public String getVersionMajor() + { + return serverProperties.getProperty("version.major"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionMinor() + */ + public String getVersionMinor() + { + return serverProperties.getProperty("version.minor"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionRevision() + */ + public String getVersionRevision() + { + return serverProperties.getProperty("version.revision"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersionLabel() + */ + public String getVersionLabel() + { + return serverProperties.getProperty("version.label"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getVersion() + */ + public String getVersion() + { + String version = getVersionMajor() + "." + getVersionMinor() + "." + getVersionRevision(); + String label = getVersionLabel(); + if (label != null && label.length() > 0) + { + version += " (" + label + ")"; + } + return version; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getEdition() + */ + public String getEdition() + { + return serverProperties.getProperty("version.edition"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getDescriptorKeys() + */ + public String[] getDescriptorKeys() + { + String[] keys = new String[serverProperties.size()]; + serverProperties.keySet().toArray(keys); + return keys; + } + + /* (non-Javadoc) + * @see org.alfresco.service.descriptor.Descriptor#getDescriptor(java.lang.String) + */ + public String getDescriptor(String key) + { + return serverProperties.getProperty(key, ""); + } + } + +} diff --git a/source/java/org/alfresco/repo/descriptor/DescriptorServiceTest.java b/source/java/org/alfresco/repo/descriptor/DescriptorServiceTest.java new file mode 100644 index 0000000000..8f41f92298 --- /dev/null +++ b/source/java/org/alfresco/repo/descriptor/DescriptorServiceTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.descriptor; + +import org.alfresco.repo.importer.ImporterBootstrap; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.debug.NodeStoreInspector; + +public class DescriptorServiceTest extends BaseSpringTest +{ + private NodeService nodeService; + private ImporterBootstrap systemBootstrap; + private StoreRef storeRef; + private AuthenticationComponent authenticationComponent; + + + @Override + protected void onSetUpInTransaction() throws Exception + { + nodeService = (NodeService)applicationContext.getBean(ServiceRegistry.NODE_SERVICE.getLocalName()); + systemBootstrap = (ImporterBootstrap)applicationContext.getBean("systemBootstrap"); + + storeRef = new StoreRef("system", "Test_" + System.currentTimeMillis()); + systemBootstrap.setStoreUrl(storeRef.toString()); + systemBootstrap.bootstrap(); + + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + + + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + } + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + + public void testDescriptor() + { + ServiceRegistry registry = (ServiceRegistry)applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + DescriptorService descriptor = registry.getDescriptorService(); + Descriptor serverDescriptor = descriptor.getDescriptor(); + + String major = serverDescriptor.getVersionMajor(); + String minor = serverDescriptor.getVersionMinor(); + String revision = serverDescriptor.getVersionRevision(); + String label = serverDescriptor.getVersionLabel(); + String version = major + "." + minor + "." + revision; + if (label != null && label.length() > 0) + { + version += " (" + label + ")"; + } + + assertEquals(version, serverDescriptor.getVersion()); + } + + public void testRepositoryDescriptor() + { + ServiceRegistry registry = (ServiceRegistry)applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + DescriptorService descriptor = registry.getDescriptorService(); + Descriptor serverDescriptor = descriptor.getRepositoryDescriptor(); + + String major = serverDescriptor.getVersionMajor(); + String minor = serverDescriptor.getVersionMinor(); + String revision = serverDescriptor.getVersionRevision(); + String label = serverDescriptor.getVersionLabel(); + String version = major + "." + minor + "." + revision; + if (label != null && label.length() > 0) + { + version += " (" + label + ")"; + } + + assertEquals(version, serverDescriptor.getVersion()); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/CompiledModel.java b/source/java/org/alfresco/repo/dictionary/CompiledModel.java new file mode 100644 index 0000000000..56f2ea9b7a --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/CompiledModel.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Compiled representation of a model definition. + * + * In this case, compiled means that + * a) all references between model items have been resolved + * b) inheritence of class features have been flattened + * c) overridden class features have been resolved + * + * A compiled model also represents a valid model. + * + * @author David Caruana + * + */ +/*package*/ class CompiledModel implements ModelQuery +{ + + // Logger + private static final Log logger = LogFactory.getLog(DictionaryDAOImpl.class); + + private M2Model model; + private ModelDefinition modelDefinition; + private Map dataTypes = new HashMap(); + private Map classes = new HashMap(); + private Map types = new HashMap(); + private Map aspects = new HashMap(); + private Map properties = new HashMap(); + private Map associations = new HashMap(); + + + /** + * Construct + * + * @param model model definition + * @param dictionaryDAO dictionary DAO + * @param namespaceDAO namespace DAO + */ + /*package*/ CompiledModel(M2Model model, DictionaryDAO dictionaryDAO, NamespaceDAO namespaceDAO) + { + try + { + // Phase 1: Construct model definitions from model entries + // resolving qualified names + this.model = model; + constructDefinitions(model, dictionaryDAO, namespaceDAO); + + // Phase 2: Resolve dependencies between model definitions + ModelQuery query = new DelegateModelQuery(this, dictionaryDAO); + resolveDependencies(query); + + // Phase 3: Resolve inheritance of values within class hierachy + resolveInheritance(query); + } + catch(Exception e) + { + throw new DictionaryException("Failed to compile model " + model.getName(), e); + } + } + + + /** + * @return the model definition + */ + /*package*/ M2Model getM2Model() + { + return model; + } + + + /** + * Construct compiled definitions + * + * @param model model definition + * @param dictionaryDAO dictionary DAO + * @param namespaceDAO namespace DAO + */ + private void constructDefinitions(M2Model model, DictionaryDAO dictionaryDAO, NamespaceDAO namespaceDAO) + { + NamespacePrefixResolver localPrefixes = createLocalPrefixResolver(model, namespaceDAO); + + // Construct Model Definition + modelDefinition = new M2ModelDefinition(model, localPrefixes); + + // Construct Property Types + for (M2DataType propType : model.getPropertyTypes()) + { + M2DataTypeDefinition def = new M2DataTypeDefinition(modelDefinition, propType, localPrefixes); + if (dataTypes.containsKey(def.getName())) + { + throw new DictionaryException("Found duplicate property type definition " + propType.getName()); + } + dataTypes.put(def.getName(), def); + } + + // Construct Type Definitions + for (M2Type type : model.getTypes()) + { + M2TypeDefinition def = new M2TypeDefinition(modelDefinition, type, localPrefixes, properties, associations); + if (classes.containsKey(def.getName())) + { + throw new DictionaryException("Found duplicate class definition " + type.getName() + " (a type)"); + } + classes.put(def.getName(), def); + types.put(def.getName(), def); + } + + // Construct Aspect Definitions + for (M2Aspect aspect : model.getAspects()) + { + M2AspectDefinition def = new M2AspectDefinition(modelDefinition, aspect, localPrefixes, properties, associations); + if (classes.containsKey(def.getName())) + { + throw new DictionaryException("Found duplicate class definition " + aspect.getName() + " (an aspect)"); + } + classes.put(def.getName(), def); + aspects.put(def.getName(), def); + } + } + + + /** + * Create a local namespace prefix resolver containing the namespaces defined and imported + * in the model + * + * @param model model definition + * @param namespaceDAO namespace DAO + * @return the local namespace prefix resolver + */ + private NamespacePrefixResolver createLocalPrefixResolver(M2Model model, NamespaceDAO namespaceDAO) + { + // Retrieve set of existing URIs for validation purposes + Collection uris = namespaceDAO.getURIs(); + + // Create a namespace prefix resolver based on imported and defined + // namespaces within the model + DynamicNamespacePrefixResolver prefixResolver = new DynamicNamespacePrefixResolver(null); + for (M2Namespace imported : model.getImports()) + { + String uri = imported.getUri(); + if (!uris.contains(uri)) + { + throw new NamespaceException("URI " + uri + " cannot be imported as it is not defined (with prefix " + imported.getPrefix()); + } + prefixResolver.registerNamespace(imported.getPrefix(), uri); + } + for (M2Namespace defined : model.getNamespaces()) + { + prefixResolver.registerNamespace(defined.getPrefix(), defined.getUri()); + } + return prefixResolver; + } + + + /** + * Resolve dependencies between model items + * + * @param query support for querying other items in model + */ + private void resolveDependencies(ModelQuery query) + { + for (DataTypeDefinition def : dataTypes.values()) + { + ((M2DataTypeDefinition)def).resolveDependencies(query); + } + for (ClassDefinition def : classes.values()) + { + ((M2ClassDefinition)def).resolveDependencies(query); + } + } + + + /** + * Resolve class feature inheritence + * + * @param query support for querying other items in model + */ + private void resolveInheritance(ModelQuery query) + { + // Calculate order of class processing (root to leaf) + Map> order = new TreeMap>(); + for (ClassDefinition def : classes.values()) + { + // Calculate class depth in hierarchy + int depth = 0; + QName parentName = def.getParentName(); + while (parentName != null) + { + ClassDefinition parentClass = getClass(parentName); + if (parentClass == null) + { + break; + } + depth = depth +1; + parentName = parentClass.getParentName(); + } + + // Map class to depth + List classes = order.get(depth); + if (classes == null) + { + classes = new ArrayList(); + order.put(depth, classes); + } + classes.add(def); + + if (logger.isDebugEnabled()) + logger.debug("Resolving inheritance: class " + def.getName() + " found at depth " + depth); + } + + // Resolve inheritance of each class + for (int depth = 0; depth < order.size(); depth++) + { + for (ClassDefinition def : order.get(depth)) + { + ((M2ClassDefinition)def).resolveInheritance(query); + } + } + } + + + /** + * @return the compiled model definition + */ + public ModelDefinition getModelDefinition() + { + return modelDefinition; + } + + + /** + * @return the compiled property types + */ + public Collection getDataTypes() + { + return dataTypes.values(); + } + + + /** + * @return the compiled types + */ + public Collection getTypes() + { + return types.values(); + } + + + /** + * @return the compiled aspects + */ + public Collection getAspects() + { + return aspects.values(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getPropertyType(org.alfresco.repo.ref.QName) + */ + public DataTypeDefinition getDataType(QName name) + { + return dataTypes.get(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelQuery#getDataType(java.lang.Class) + */ + public DataTypeDefinition getDataType(Class javaClass) + { + for (DataTypeDefinition dataTypeDef : dataTypes.values()) + { + if (dataTypeDef.getJavaClassName().equals(javaClass.getName())) + { + return dataTypeDef; + } + } + return null; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getType(org.alfresco.repo.ref.QName) + */ + public TypeDefinition getType(QName name) + { + return types.get(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getAspect(org.alfresco.repo.ref.QName) + */ + public AspectDefinition getAspect(QName name) + { + return aspects.get(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getClass(org.alfresco.repo.ref.QName) + */ + public ClassDefinition getClass(QName name) + { + return classes.get(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getProperty(org.alfresco.repo.ref.QName) + */ + public PropertyDefinition getProperty(QName name) + { + return properties.get(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getAssociation(org.alfresco.repo.ref.QName) + */ + public AssociationDefinition getAssociation(QName name) + { + return associations.get(name); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/DelegateModelQuery.java b/source/java/org/alfresco/repo/dictionary/DelegateModelQuery.java new file mode 100644 index 0000000000..4a8931e443 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DelegateModelQuery.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; + +/** + * Model query that delegates its search if itself cannot find the model + * item required. + * + * @author David Caruana + * + */ +/*package*/ class DelegateModelQuery implements ModelQuery +{ + + private ModelQuery query; + private ModelQuery delegate; + + + /** + * Construct + * + * @param query + * @param delegate + */ + /*package*/ DelegateModelQuery(ModelQuery query, ModelQuery delegate) + { + this.query = query; + this.delegate = delegate; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getPropertyType(org.alfresco.repo.ref.QName) + */ + public DataTypeDefinition getDataType(QName name) + { + DataTypeDefinition def = query.getDataType(name); + if (def == null) + { + def = delegate.getDataType(name); + } + return def; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelQuery#getDataType(java.lang.Class) + */ + public DataTypeDefinition getDataType(Class javaClass) + { + DataTypeDefinition def = query.getDataType(javaClass); + if (def == null) + { + def = delegate.getDataType(javaClass); + } + return def; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getType(org.alfresco.repo.ref.QName) + */ + public TypeDefinition getType(QName name) + { + TypeDefinition def = query.getType(name); + if (def == null) + { + def = delegate.getType(name); + } + return def; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getAspect(org.alfresco.repo.ref.QName) + */ + public AspectDefinition getAspect(QName name) + { + AspectDefinition def = query.getAspect(name); + if (def == null) + { + def = delegate.getAspect(name); + } + return def; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getClass(org.alfresco.repo.ref.QName) + */ + public ClassDefinition getClass(QName name) + { + ClassDefinition def = query.getClass(name); + if (def == null) + { + def = delegate.getClass(name); + } + return def; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getProperty(org.alfresco.repo.ref.QName) + */ + public PropertyDefinition getProperty(QName name) + { + PropertyDefinition def = query.getProperty(name); + if (def == null) + { + def = delegate.getProperty(name); + } + return def; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getAssociation(org.alfresco.repo.ref.QName) + */ + public AssociationDefinition getAssociation(QName name) + { + AssociationDefinition def = query.getAssociation(name); + if (def == null) + { + def = delegate.getAssociation(name); + } + return def; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryBootstrap.java b/source/java/org/alfresco/repo/dictionary/DictionaryBootstrap.java new file mode 100644 index 0000000000..f91b09318a --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryBootstrap.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.cmr.dictionary.DictionaryException; + + +/** + * Bootstrap Dictionary DAO with pre-defined models + * + * @author David Caruana + * + */ +public class DictionaryBootstrap +{ + // The list of models to bootstrap with + private List models = new ArrayList(); + + // The list of model resource bundles to bootstrap with + private List resourceBundles = new ArrayList(); + + // Dictionary DAO + private DictionaryDAO dictionaryDAO = null; + + /** + * Sets the Dictionary DAO + * + * @param dictionaryDAO + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + /** + * Sets the initial list of models to bootstrap with + * + * @param modelResources the model names + */ + public void setModels(List modelResources) + { + this.models = modelResources; + } + + /** + * Sets the initial list of models to bootstrap with + * + * @param modelResources the model names + */ + public void setLabels(List labels) + { + this.resourceBundles = labels; + } + + /** + * Bootstrap the Dictionary + */ + public void bootstrap() + { + // register models + for (String bootstrapModel : models) + { + InputStream modelStream = getClass().getClassLoader().getResourceAsStream(bootstrapModel); + if (modelStream == null) + { + throw new DictionaryException("Could not find bootstrap model " + bootstrapModel); + } + try + { + M2Model model = M2Model.createModel(modelStream); + dictionaryDAO.putModel(model); + } + catch(DictionaryException e) + { + throw new DictionaryException("Could not import bootstrap model " + bootstrapModel, e); + } + } + + // register models + for (String resourceBundle : resourceBundles) + { + I18NUtil.registerResourceBundle(resourceBundle); + } + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryComponent.java b/source/java/org/alfresco/repo/dictionary/DictionaryComponent.java new file mode 100644 index 0000000000..7c7493e609 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryComponent.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; + + +/** + * Data Dictionary Service Implementation + * + * @author David Caruana + */ +public class DictionaryComponent implements DictionaryService +{ + private DictionaryDAO dictionaryDAO; + + + // TODO: Check passed arguments are valid + + /** + * Sets the Meta Model DAO + * + * @param metaModelDAO meta model DAO + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAllModels() + */ + public Collection getAllModels() + { + return dictionaryDAO.getModels(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getModel(org.alfresco.repo.ref.QName) + */ + public ModelDefinition getModel(QName model) + { + return dictionaryDAO.getModel(model); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAllPropertyTypes() + */ + public Collection getAllDataTypes() + { + Collection propertyTypes = new ArrayList(); + for (QName model : getAllModels()) + { + propertyTypes.addAll(getAspects(model)); + } + return propertyTypes; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getPropertyTypes(org.alfresco.repo.ref.QName) + */ + public Collection getDataTypes(QName model) + { + Collection propertyTypes = dictionaryDAO.getDataTypes(model); + Collection qnames = new ArrayList(propertyTypes.size()); + for (DataTypeDefinition def : propertyTypes) + { + qnames.add(def.getName()); + } + return qnames; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAllTypes() + */ + public Collection getAllTypes() + { + Collection types = new ArrayList(); + for (QName model : getAllModels()) + { + types.addAll(getTypes(model)); + } + return types; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getTypes(org.alfresco.repo.ref.QName) + */ + public Collection getTypes(QName model) + { + Collection types = dictionaryDAO.getTypes(model); + Collection qnames = new ArrayList(types.size()); + for (TypeDefinition def : types) + { + qnames.add(def.getName()); + } + return qnames; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAllAspects() + */ + public Collection getAllAspects() + { + Collection aspects = new ArrayList(); + for (QName model : getAllModels()) + { + aspects.addAll(getAspects(model)); + } + return aspects; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAspects(org.alfresco.repo.ref.QName) + */ + public Collection getAspects(QName model) + { + Collection aspects = dictionaryDAO.getAspects(model); + Collection qnames = new ArrayList(aspects.size()); + for (AspectDefinition def : aspects) + { + qnames.add(def.getName()); + } + return qnames; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#isSubClass(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName) + */ + public boolean isSubClass(QName className, QName ofClassName) + { + // Validate arguments + ParameterCheck.mandatory("className", className); + ParameterCheck.mandatory("ofClassName", ofClassName); + ClassDefinition classDef = getClass(className); + if (classDef == null) + { + throw new InvalidTypeException(className); + } + ClassDefinition ofClassDef = getClass(ofClassName); + if (ofClassDef == null) + { + throw new InvalidTypeException(ofClassName); + } + + // Only check if both ends are either a type or an aspect + boolean subClassOf = false; + if (classDef.isAspect() == ofClassDef.isAspect()) + { + while (classDef != null) + { + if (classDef.equals(ofClassDef)) + { + subClassOf = true; + break; + } + + // No match yet, so go to parent class + QName parentClassName = classDef.getParentName(); + classDef = (parentClassName == null) ? null : getClass(parentClassName); + } + } + return subClassOf; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getPropertyType(org.alfresco.repo.ref.QName) + */ + public DataTypeDefinition getDataType(QName name) + { + return dictionaryDAO.getDataType(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.DictionaryService#getDataType(java.lang.Class) + */ + public DataTypeDefinition getDataType(Class javaClass) + { + return dictionaryDAO.getDataType(javaClass); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getType(org.alfresco.repo.ref.QName) + */ + public TypeDefinition getType(QName name) + { + return dictionaryDAO.getType(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAspect(org.alfresco.repo.ref.QName) + */ + public AspectDefinition getAspect(QName name) + { + return dictionaryDAO.getAspect(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getClass(org.alfresco.repo.ref.QName) + */ + public ClassDefinition getClass(QName name) + { + return dictionaryDAO.getClass(name); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAnonymousType(org.alfresco.repo.ref.QName, java.util.Collection) + */ + public TypeDefinition getAnonymousType(QName type, Collection aspects) + { + return dictionaryDAO.getAnonymousType(type, aspects); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getProperty(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName) + */ + public PropertyDefinition getProperty(QName className, QName propertyName) + { + PropertyDefinition propDef = null; + ClassDefinition classDef = dictionaryDAO.getClass(className); + if (classDef != null) + { + Map propDefs = classDef.getProperties(); + propDef = propDefs.get(propertyName); + } + return propDef; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getProperty(org.alfresco.repo.ref.QName) + */ + public PropertyDefinition getProperty(QName propertyName) + { + return dictionaryDAO.getProperty(propertyName); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.DictionaryService#getAssociation(org.alfresco.repo.ref.QName) + */ + public AssociationDefinition getAssociation(QName associationName) + { + return dictionaryDAO.getAssociation(associationName); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryDAO.java b/source/java/org/alfresco/repo/dictionary/DictionaryDAO.java new file mode 100644 index 0000000000..cc97e12f18 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryDAO.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Collection; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; + + +/** + * Dictionary Data Access + * + * @author David Caruana + */ +public interface DictionaryDAO extends ModelQuery +{ + + /** + * @return the models known by the dictionary + */ + public Collection getModels(); + + /** + * @param name the model to retrieve + * @return the named model definition + */ + public ModelDefinition getModel(QName name); + + /** + * @param model the model to retrieve property types for + * @return the property types of the model + */ + public Collection getDataTypes(QName model); + + /** + * @param model the model to retrieve types for + * @return the types of the model + */ + public Collection getTypes(QName model); + + /** + * @param model the model to retrieve aspects for + * @return the aspects of the model + */ + public Collection getAspects(QName model); + + /** + * Construct an anonymous type that combines a primary type definition and + * and one or more aspects + * + * @param type the primary type + * @param aspects the aspects to combine + * @return the anonymous type definition + */ + public TypeDefinition getAnonymousType(QName type, Collection aspects); + + /** + * Adds a model to the dictionary. The model is compiled and validated. + * + * @param model the model to add + */ + public void putModel(M2Model model); + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryDAOImpl.java b/source/java/org/alfresco/repo/dictionary/DictionaryDAOImpl.java new file mode 100644 index 0000000000..a3a150a245 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryDAOImpl.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; + + +/** + * Default implementation of the Dictionary. + * + * @author David Caruana + * + */ +public class DictionaryDAOImpl implements DictionaryDAO +{ + // TODO: Allow for the dynamic creation of models. Supporting + // this requires the ability to persistently store the + // registration of models, the ability to load models + // from a persistent store, the refresh of the cache + // and concurrent read/write of the models. + + // Namespace Data Access + private NamespaceDAO namespaceDAO; + + // Map of namespace to model name + private Map namespaceToModel = new HashMap(); + + // Map of model name to compiled model + private Map compiledModels = new HashMap(); + + + /** + * Construct + * + * @param namespaceDAO namespace data access + */ + public DictionaryDAOImpl(NamespaceDAO namespaceDAO) + { + this.namespaceDAO = namespaceDAO; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#putModel(org.alfresco.repo.dictionary.impl.M2Model) + */ + public void putModel(M2Model model) + { + // Compile model definition + CompiledModel compiledModel = model.compile(this, namespaceDAO); + QName modelName = compiledModel.getModelDefinition().getName(); + + // Remove namespace definitions for previous model, if it exists + CompiledModel previousVersion = compiledModels.get(modelName); + if (previousVersion != null) + { + for (M2Namespace namespace : previousVersion.getM2Model().getNamespaces()) + { + namespaceDAO.removePrefix(namespace.getPrefix()); + namespaceDAO.removeURI(namespace.getUri()); + namespaceToModel.remove(namespace.getUri()); + } + } + + // Create namespace definitions for new model + for (M2Namespace namespace : model.getNamespaces()) + { + namespaceDAO.addURI(namespace.getUri()); + namespaceDAO.addPrefix(namespace.getPrefix(), namespace.getUri()); + namespaceToModel.put(namespace.getUri(), modelName); + } + + // Publish new Model Definition + compiledModels.put(modelName, compiledModel); + } + + + /** + * @param uri the namespace uri + * @return the compiled model which defines the specified namespace + */ + private CompiledModel getCompiledModelForNamespace(String uri) + { + QName modelName = namespaceToModel.get(uri); + return (modelName == null) ? null : getCompiledModel(modelName); + } + + + /** + * @param modelName the model name + * @return the compiled model of the given name + */ + private CompiledModel getCompiledModel(QName modelName) + { + CompiledModel model = compiledModels.get(modelName); + if (model == null) + { + // TODO: Load model from persistent store + throw new DictionaryException("Model " + modelName + " does not exist"); + } + return model; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getPropertyType(org.alfresco.repo.ref.QName) + */ + public DataTypeDefinition getDataType(QName typeName) + { + CompiledModel model = getCompiledModelForNamespace(typeName.getNamespaceURI()); + return (model == null) ? null : model.getDataType(typeName); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelQuery#getDataType(java.lang.Class) + */ + public DataTypeDefinition getDataType(Class javaClass) + { + for (CompiledModel model : compiledModels.values()) + { + DataTypeDefinition dataTypeDef = model.getDataType(javaClass); + if (dataTypeDef != null) + { + return dataTypeDef; + } + } + return null; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#getPropertyTypes(org.alfresco.repo.ref.QName) + */ + public Collection getDataTypes(QName modelName) + { + CompiledModel model = getCompiledModel(modelName); + return model.getDataTypes(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getType(org.alfresco.repo.ref.QName) + */ + public TypeDefinition getType(QName typeName) + { + CompiledModel model = getCompiledModelForNamespace(typeName.getNamespaceURI()); + return (model == null) ? null : model.getType(typeName); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getAspect(org.alfresco.repo.ref.QName) + */ + public AspectDefinition getAspect(QName aspectName) + { + CompiledModel model = getCompiledModelForNamespace(aspectName.getNamespaceURI()); + return (model == null) ? null : model.getAspect(aspectName); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getClass(org.alfresco.repo.ref.QName) + */ + public ClassDefinition getClass(QName className) + { + CompiledModel model = getCompiledModelForNamespace(className.getNamespaceURI()); + return (model == null) ? null : model.getClass(className); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getProperty(org.alfresco.repo.ref.QName) + */ + public PropertyDefinition getProperty(QName propertyName) + { + CompiledModel model = getCompiledModelForNamespace(propertyName.getNamespaceURI()); + return (model == null) ? null : model.getProperty(propertyName); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.ModelQuery#getAssociation(org.alfresco.repo.ref.QName) + */ + public AssociationDefinition getAssociation(QName assocName) + { + CompiledModel model = getCompiledModelForNamespace(assocName.getNamespaceURI()); + return (model == null) ? null : model.getAssociation(assocName); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#getModels() + */ + public Collection getModels() + { + return compiledModels.keySet(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#getModel(org.alfresco.repo.ref.QName) + */ + public ModelDefinition getModel(QName name) + { + CompiledModel model = getCompiledModel(name); + if (model != null) + { + return model.getModelDefinition(); + } + return null; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#getTypes(org.alfresco.repo.ref.QName) + */ + public Collection getTypes(QName modelName) + { + CompiledModel model = getCompiledModel(modelName); + return model.getTypes(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#getAspects(org.alfresco.repo.ref.QName) + */ + public Collection getAspects(QName modelName) + { + CompiledModel model = getCompiledModel(modelName); + return model.getAspects(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.DictionaryDAO#getAnonymousType(org.alfresco.repo.ref.QName, java.util.Collection) + */ + public TypeDefinition getAnonymousType(QName type, Collection aspects) + { + TypeDefinition typeDef = getType(type); + if (typeDef == null) + { + throw new DictionaryException("Failed to create anonymous type as specified type " + type + " not found"); + } + Collection aspectDefs = new ArrayList(); + if (aspects != null) + { + for (QName aspect : aspects) + { + AspectDefinition aspectDef = getAspect(aspect); + if (typeDef == null) + { + throw new DictionaryException("Failed to create anonymous type as specified aspect " + aspect + " not found"); + } + aspectDefs.add(aspectDef); + } + } + return new M2AnonymousTypeDefinition(typeDef, aspectDefs); + } + + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java b/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java new file mode 100644 index 0000000000..e1f3a9df2f --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; + + +public class DictionaryDAOTest extends TestCase +{ + + private static final String TEST_MODEL = "org/alfresco/repo/dictionary/dictionarydaotest_model.xml"; + private static final String TEST_BUNDLE = "org/alfresco/repo/dictionary/dictionarydaotest_model"; + private DictionaryService service; + + + @Override + public void setUp() + { + // Instantiate Dictionary Service + NamespaceDAO namespaceDAO = new NamespaceDAOImpl(); + DictionaryDAOImpl dictionaryDAO = new DictionaryDAOImpl(namespaceDAO); + + // Populate with appropriate models + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + List bootstrapModels = new ArrayList(); + bootstrapModels.add("alfresco/model/dictionaryModel.xml"); + bootstrapModels.add(TEST_MODEL); + List labels = new ArrayList(); + labels.add(TEST_BUNDLE); + bootstrap.setModels(bootstrapModels); + bootstrap.setLabels(labels); + bootstrap.setDictionaryDAO(dictionaryDAO); + bootstrap.bootstrap(); + + DictionaryComponent component = new DictionaryComponent(); + component.setDictionaryDAO(dictionaryDAO); + service = component; + } + + + public void testBootstrap() + { + NamespaceDAO namespaceDAO = new NamespaceDAOImpl(); + DictionaryDAOImpl dictionaryDAO = new DictionaryDAOImpl(namespaceDAO); + + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + List bootstrapModels = new ArrayList(); + + bootstrapModels.add("alfresco/model/dictionaryModel.xml"); + bootstrapModels.add("alfresco/model/systemModel.xml"); + bootstrapModels.add("alfresco/model/contentModel.xml"); + bootstrapModels.add("alfresco/model/applicationModel.xml"); + + bootstrapModels.add("alfresco/extension/exampleModel.xml"); + + bootstrapModels.add("org/alfresco/repo/security/authentication/userModel.xml"); + bootstrapModels.add("org/alfresco/repo/action/actionModel.xml"); + bootstrapModels.add("org/alfresco/repo/rule/ruleModel.xml"); + bootstrapModels.add("org/alfresco/repo/version/version_model.xml"); + + bootstrap.setModels(bootstrapModels); + bootstrap.setDictionaryDAO(dictionaryDAO); + bootstrap.bootstrap(); + } + + + public void testLabels() + { + QName model = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "dictionarydaotest"); + ModelDefinition modelDef = service.getModel(model); + assertEquals("Model Description", modelDef.getDescription()); + QName type = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "base"); + TypeDefinition typeDef = service.getType(type); + assertEquals("Base Title", typeDef.getTitle()); + assertEquals("Base Description", typeDef.getDescription()); + QName prop = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "prop1"); + PropertyDefinition propDef = service.getProperty(prop); + assertEquals("Prop1 Title", propDef.getTitle()); + assertEquals("Prop1 Description", propDef.getDescription()); + QName assoc = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "assoc1"); + AssociationDefinition assocDef = service.getAssociation(assoc); + assertEquals("Assoc1 Title", assocDef.getTitle()); + assertEquals("Assoc1 Description", assocDef.getDescription()); + QName datatype = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "datatype"); + DataTypeDefinition datatypeDef = service.getDataType(datatype); + assertEquals("Datatype Analyser", datatypeDef.getAnalyserClassName()); + } + + + public void testSubClassOf() + { + QName invalid = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "invalid"); + QName base = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "base"); + QName file = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "file"); + QName folder = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "folder"); + QName referenceable = QName.createQName("http://www.alfresco.org/test/dictionarydaotest/1.0", "referenceable"); + + // Test invalid args + try + { + service.isSubClass(invalid, referenceable); + fail("Failed to catch invalid class parameter"); + } + catch(InvalidTypeException e) {} + + try + { + service.isSubClass(referenceable, invalid); + fail("Failed to catch invalid class parameter"); + } + catch(InvalidTypeException e) {} + + // Test various flavours of subclassof + boolean test1 = service.isSubClass(file, referenceable); // type vs aspect + assertFalse(test1); + boolean test2 = service.isSubClass(file, folder); // seperate hierarchies + assertFalse(test2); + boolean test3 = service.isSubClass(file, file); // self + assertTrue(test3); + boolean test4 = service.isSubClass(folder, base); // subclass + assertTrue(test4); + boolean test5 = service.isSubClass(base, folder); // reversed test + assertFalse(test5); + } + + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java b/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java new file mode 100644 index 0000000000..7177bb5be6 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryModelType.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; + +/** + * Dictionary model type behaviour. + * + * @author Roy Wetherall + */ +public class DictionaryModelType +{ + /** Key to the pending models */ + private static final String KEY_PENDING_MODELS = "dictionaryModelType.pendingModels"; + + /** The dictionary DAO */ + private DictionaryDAO dictionaryDAO; + + /** The namespace DAO */ + private NamespaceDAO namespaceDAO; + + /** The node service */ + private NodeService nodeService; + + /** The content service */ + private ContentService contentService; + + /** The policy component */ + private PolicyComponent policyComponent; + + /** Transaction listener */ + private DictionaryModelTypeTransactionListener transactionListener; + + /** + * Set the dictionary DAO + * + * @param dictionaryDAO the dictionary DAO + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + /** + * Set the namespace DOA + * + * @param namespaceDAO the namespace DAO + */ + public void setNamespaceDAO(NamespaceDAO namespaceDAO) + { + this.namespaceDAO = namespaceDAO; + } + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the content service + * + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Set the policy component + * + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * The initialise method + */ + public void init() + { + // Register interest in the onContentUpdate policy for the dictionary model type + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onContentUpdate"), + ContentModel.TYPE_DICTIONARY_MODEL, + new JavaBehaviour(this, "onContentUpdate")); + + // Create the transaction listener + this.transactionListener = new DictionaryModelTypeTransactionListener(this.nodeService, this.contentService); + } + + /** + * On content update behaviour implementation + * + * @param nodeRef the node reference whose content has been updated + */ + @SuppressWarnings("unchecked") + public void onContentUpdate(NodeRef nodeRef) + { + Set pendingModelUpdates = (Set)AlfrescoTransactionSupport.getResource(KEY_PENDING_MODELS); + if (pendingModelUpdates == null) + { + pendingModelUpdates = new HashSet(); + AlfrescoTransactionSupport.bindResource(KEY_PENDING_MODELS, pendingModelUpdates); + } + pendingModelUpdates.add(nodeRef); + + AlfrescoTransactionSupport.bindListener(this.transactionListener); + } + + // TODO need to listen for a change in the modelActive attribute and update appropriatly + + // TODO need to listen for node deletion and act accordingly + + /** + * Dictionary model type transaction listener class. + */ + public class DictionaryModelTypeTransactionListener implements TransactionListener + { + /** + * Id used in equals and hash + */ + private String id = GUID.generate(); + + private NodeService nodeService; + private ContentService contentService; + + public DictionaryModelTypeTransactionListener(NodeService nodeService, ContentService contentService) + { + this.nodeService = nodeService; + this.contentService = contentService; + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#flush() + */ + public void flush() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#beforeCommit(boolean) + */ + @SuppressWarnings("unchecked") + public void beforeCommit(boolean readOnly) + { + Set pendingModels = (Set)AlfrescoTransactionSupport.getResource(KEY_PENDING_MODELS); + + if (pendingModels != null) + { + for (NodeRef nodeRef : pendingModels) + { + // Find out whether the model is active (by default it is) + boolean isActive = true; + Boolean value = (Boolean)nodeService.getProperty(nodeRef, ContentModel.PROP_MODEL_ACTIVE); + if (value != null) + { + isActive = value.booleanValue(); + } + + // Ignore if the node is a working copy or if its inactive + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY) == false && + isActive == true) + { + // 1. Compile the model and update the details on the node + // 2. Re-put the model + + ContentReader contentReader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + if (contentReader != null) + { + // Create a model from the current content + M2Model m2Model = M2Model.createModel(contentReader.getContentInputStream()); + // TODO what do we do if we don't have a model?? + + // Try and compile the model + ModelDefinition modelDefintion = m2Model.compile(dictionaryDAO, namespaceDAO).getModelDefinition(); + // TODO what do we do if the model does not compile + + // Update the meta data for the model + Map props = this.nodeService.getProperties(nodeRef); + props.put(ContentModel.PROP_MODEL_NAME, modelDefintion.getName()); + props.put(ContentModel.PROP_MODEL_DESCRIPTION, modelDefintion.getDescription()); + props.put(ContentModel.PROP_MODEL_AUTHOR, modelDefintion.getAuthor()); + props.put(ContentModel.PROP_MODEL_PUBLISHED_DATE, modelDefintion.getPublishedDate()); + props.put(ContentModel.PROP_MODEL_VERSION, modelDefintion.getVersion()); + this.nodeService.setProperties(nodeRef, props); + + // TODO how do we get the dependancies for this model ?? + + // Put the model + dictionaryDAO.putModel(m2Model); + } + } + } + } + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#beforeCompletion() + */ + public void beforeCompletion() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#afterCommit() + */ + public void afterCommit() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#afterRollback() + */ + public void afterRollback() + { + } + + /** + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof DictionaryModelTypeTransactionListener) + { + DictionaryModelTypeTransactionListener that = (DictionaryModelTypeTransactionListener) obj; + return (this.id.equals(that.id)); + } + else + { + return false; + } + } + } +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java b/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java new file mode 100644 index 0000000000..848a1c730e --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryModelTypeTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.PropertyMap; + +/** + * Dictionary model type unit test + * + * @author Roy Wetherall + */ +public class DictionaryModelTypeTest extends BaseAlfrescoSpringTest +{ + /** QName of the test model */ + private static final QName TEST_MODEL_ONE = QName.createQName("{http://www.alfresco.org/test/testmodel1/1.0}testModelOne"); + + /** Test model XML */ + public static final String MODEL_ONE_XML = + "" + + + " Test model one" + + " Alfresco" + + " 2005-05-30" + + " 1.0" + + + " " + + " " + + " " + + + " " + + " " + + " " + + + " " + + + " " + + " Base" + + " The Base Type" + + " " + + " " + + " d:text" + + " " + + " " + + " " + + + " " + + + ""; + + public static final String MODEL_ONE_MODIFIED_XML = + "" + + + " Test model one (updated)" + + " Alfresco" + + " 2005-05-30" + + " 1.1" + + + " " + + " " + + " " + + + " " + + " " + + " " + + + " " + + + " " + + " Base" + + " The Base Type" + + " " + + " " + + " d:text" + + " " + + " " + + " d:text" + + " " + + " " + + " " + + + " " + + + ""; + + /** Services used in tests */ + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + private CheckOutCheckInService cociService; + + /** + * On setup in transaction override + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + + super.onSetUpInTransaction(); + + // Get the required services + this.dictionaryService = (DictionaryService)this.applicationContext.getBean("dictionaryService"); + this.namespaceService = (NamespaceService)this.applicationContext.getBean("namespaceService"); + this.cociService = (CheckOutCheckInService)this.applicationContext.getBean("checkOutCheckInService"); + + } + + /** + * Test the creation of dictionary model nodes + */ + public void testCreateAndUpdateDictionaryModelNodeContent() + { + try + { + // Check that the model has not yet been loaded into the dictionary + this.dictionaryService.getModel(TEST_MODEL_ONE); + fail("This model has not yet been loaded into the dictionary service"); + } + catch (DictionaryException exception) + { + // We expect this exception + } + + // Check that the namespace is not yet in the namespace service + String uri = this.namespaceService.getNamespaceURI("test1"); + assertNull(uri); + + // Create a model node + PropertyMap properties = new PropertyMap(1); + properties.put(ContentModel.PROP_MODEL_ACTIVE, true); + final NodeRef modelNode = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.ALFRESCO_URI, "dictionaryModels"), + ContentModel.TYPE_DICTIONARY_MODEL, + properties).getChildRef(); + assertNotNull(modelNode); + + // Add the model content to the model node + ContentWriter contentWriter = this.contentService.getWriter(modelNode, ContentModel.PROP_CONTENT, true); + contentWriter.setEncoding("UTF-8"); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_XML); + contentWriter.putContent(MODEL_ONE_XML); + + // End the transaction to force update + setComplete(); + endTransaction(); + + final NodeRef workingCopy = TransactionUtil.executeInUserTransaction(this.transactionService, new TransactionUtil.TransactionWork() + { + public NodeRef doWork() throws Exception + { + // Check that the meta data has been extracted from the model + assertEquals(QName.createQName("{http://www.alfresco.org/test/testmodel1/1.0}testModelOne"), + DictionaryModelTypeTest.this.nodeService.getProperty(modelNode, ContentModel.PROP_MODEL_NAME)); + assertEquals("Test model one", DictionaryModelTypeTest.this.nodeService.getProperty(modelNode, ContentModel.PROP_MODEL_DESCRIPTION)); + assertEquals("Alfresco", DictionaryModelTypeTest.this.nodeService.getProperty(modelNode, ContentModel.PROP_MODEL_AUTHOR)); + //System.out.println(this.nodeService.getProperty(modelNode, ContentModel.PROP_MODEL_PUBLISHED_DATE)); + assertEquals("1.0", DictionaryModelTypeTest.this.nodeService.getProperty(modelNode, ContentModel.PROP_MODEL_VERSION)); + + // Check that the model is now available from the dictionary + ModelDefinition modelDefinition2 = DictionaryModelTypeTest.this.dictionaryService.getModel(TEST_MODEL_ONE); + assertNotNull(modelDefinition2); + assertEquals("Test model one", modelDefinition2.getDescription()); + + // Check that the namespace has been added to the namespace service + String uri2 = DictionaryModelTypeTest.this.namespaceService.getNamespaceURI("test1"); + assertEquals(uri2, "http://www.alfresco.org/test/testmodel1/1.0"); + + // Lets check the node out and update the content + NodeRef workingCopy = DictionaryModelTypeTest.this.cociService.checkout(modelNode); + ContentWriter contentWriter2 = DictionaryModelTypeTest.this.contentService.getWriter(workingCopy, ContentModel.PROP_CONTENT, true); + contentWriter2.putContent(MODEL_ONE_MODIFIED_XML); + + return workingCopy; + } + }); + + TransactionUtil.executeInUserTransaction(this.transactionService, new TransactionUtil.TransactionWork() + { + public Object doWork() throws Exception + { + // Check that the policy has not been fired since we have updated a working copy + assertEquals("1.0", DictionaryModelTypeTest.this.nodeService.getProperty(workingCopy, ContentModel.PROP_MODEL_VERSION)); + + // Now check the model changed back in + DictionaryModelTypeTest.this.cociService.checkin(workingCopy, null); + return null; + } + }); + + TransactionUtil.executeInUserTransaction(this.transactionService, new TransactionUtil.TransactionWork() + { + public Object doWork() throws Exception + { + // Now check that the model has been updated + assertEquals("1.1", DictionaryModelTypeTest.this.nodeService.getProperty(modelNode, ContentModel.PROP_MODEL_VERSION)); + return null; + } + }); + + } +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryNamespaceComponent.java b/source/java/org/alfresco/repo/dictionary/DictionaryNamespaceComponent.java new file mode 100644 index 0000000000..1217cabb77 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryNamespaceComponent.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Collection; + +import org.alfresco.service.namespace.NamespaceService; + + +/** + * Data Dictionary Namespace Service Implementation + * + * @author David Caruana + */ +public class DictionaryNamespaceComponent implements NamespaceService +{ + + /** + * Namespace DAO + */ + private NamespaceDAO namespaceDAO; + + + /** + * Sets the Namespace DAO + * + * @param namespaceDAO namespace DAO + */ + public void setNamespaceDAO(NamespaceDAO namespaceDAO) + { + this.namespaceDAO = namespaceDAO; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.NamespaceService#getURIs() + */ + public Collection getURIs() + { + return namespaceDAO.getURIs(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.NamespaceService#getPrefixes() + */ + public Collection getPrefixes() + { + return namespaceDAO.getPrefixes(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.ref.NamespacePrefixResolver#getNamespaceURI(java.lang.String) + */ + public String getNamespaceURI(String prefix) + { + return namespaceDAO.getNamespaceURI(prefix); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.ref.NamespacePrefixResolver#getPrefixes(java.lang.String) + */ + public Collection getPrefixes(String namespaceURI) + { + return namespaceDAO.getPrefixes(namespaceURI); + } + + + /* (non-Javadoc) + * @see org.alfresco.service.namespace.NamespaceService#registerNamespace(java.lang.String, java.lang.String) + */ + public void registerNamespace(String prefix, String uri) + { + // TODO: + throw new UnsupportedOperationException(); + } + + + /* (non-Javadoc) + * @see org.alfresco.service.namespace.NamespaceService#registerNamespace(java.lang.String, java.lang.String) + */ + public void unregisterNamespace(String prefix) + { + // TODO: + throw new UnsupportedOperationException(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrap.java b/source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrap.java new file mode 100644 index 0000000000..3500ffee24 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrap.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.transaction.TransactionService; + + +/** + * Bootstrap the dictionary from specified locations within the repository + * + * @author Roy Wetherall + */ +public class DictionaryRepositoryBootstrap +{ + /** Loactions in the respository fro which models should be loaded */ + private List repositoryLocations = new ArrayList(); + + /** Dictionary DAO */ + private DictionaryDAO dictionaryDAO = null; + + /** Search service */ + private SearchService searchService; + + /** The content service */ + private ContentService contentService; + + /** The transaction service */ + private TransactionService transactionService; + + /** The authentication component */ + private AuthenticationComponent authenticationComponent; + + /** + * Sets the Dictionary DAO + * + * @param dictionaryDAO + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + /** + * Set the search search service + * + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Set the content service + * + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Set the transaction service + * + * @param transactionService the transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the authentication service + * + * @param authenticationComponent the authentication component + */ + public void setAuthenticationComponent( + AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + /** + * Set the respository locations + * + * @param repositoryLocations list of the repository locaitons + */ + public void setRepositoryLocations( + List repositoryLocations) + { + this.repositoryLocations = repositoryLocations; + } + + @SuppressWarnings("unchecked") + public void bootstrap() + { + TransactionUtil.executeInUserTransaction(this.transactionService, new TransactionUtil.TransactionWork() + { + public Object doWork() throws Exception + { + DictionaryRepositoryBootstrap.this.authenticationComponent.setCurrentUser( + DictionaryRepositoryBootstrap.this.authenticationComponent.getSystemUserName()); + try + { + bootstrapImpl(); + } + finally + { + DictionaryRepositoryBootstrap.this.authenticationComponent.clearCurrentSecurityContext(); + } + return null; + } + }); + } + + /** + * Bootstrap the Dictionary + */ + public void bootstrapImpl() + { + Map modelMap = new HashMap(); + + // Register the models found in the respository + for (RepositoryLocation repositoryLocation : this.repositoryLocations) + { + ResultSet resultSet = this.searchService.query(repositoryLocation.getStoreRef(), SearchService.LANGUAGE_LUCENE, repositoryLocation.getQueryStatement()); + for (NodeRef dictionaryModel : resultSet.getNodeRefs()) + { + M2Model model = createM2Model(dictionaryModel); + if (model != null) + { + for (M2Namespace namespace : model.getNamespaces()) + { + modelMap.put(namespace.getUri(), model); + } + } + } + } + + // Load the models ensuring that they are loaded in the correct order + List loadedModels = new ArrayList(); + for (Map.Entry entry : modelMap.entrySet()) + { + loadModel(modelMap, loadedModels, entry.getValue()); + } + } + + /** + * Loads a model (and it dependants) if it does not exist in the list of loaded models. + * + * @param modelMap a map of the models to be loaded + * @param loadedModels the list of models already loaded + * @param model the model to try and load + */ + private void loadModel(Map modelMap, List loadedModels, M2Model model) + { + String modelName = model.getName(); + if (loadedModels.contains(modelName) == false) + { + for (M2Namespace importNamespace : model.getImports()) + { + M2Model importedModel = modelMap.get(importNamespace.getUri()); + if (importedModel != null) + { + // Ensure that the imported model is loaded first + loadModel(modelMap, loadedModels, importedModel); + } + // else we can assume that the imported model is already loaded, if this not the case then + // an error will be raised during compilation + } + + dictionaryDAO.putModel(model); + loadedModels.add(modelName); + } + } + + /** + * Create a M2Model from a dictionary model node + * + * @param nodeRef the dictionary model node reference + * @return the M2Model + */ + public M2Model createM2Model(NodeRef nodeRef) + { + M2Model model = null; + ContentReader contentReader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + if (contentReader != null) + { + model = M2Model.createModel(contentReader.getContentInputStream()); + } + // TODO should we inactivate the model node and put the error somewhere?? + return model; + } + + /** + * Repositotry location object, defines a location in the repository from within which dictionary models should be loaded + * for inclusion in the data dictionary. + * + * @author Roy Wetherall + */ + public class RepositoryLocation + { + /** Store protocol */ + private String storeProtocol; + + /** Store identifier */ + private String storeId; + + /** Path */ + private String path; + + /** + * Set the store protocol + * + * @param storeProtocol the store protocol + */ + public void setStoreProtocol(String storeProtocol) + { + this.storeProtocol = storeProtocol; + } + + /** + * Set the store identifier + * + * @param storeId the store identifier + */ + public void setStoreId(String storeId) + { + this.storeId = storeId; + } + + /** + * Set the path + * + * @param path the path + */ + public void setPath(String path) + { + this.path = path; + } + + /** + * Get the store reference + * + * @return the store reference + */ + public StoreRef getStoreRef() + { + return new StoreRef(this.storeProtocol, this.storeId); + } + + /** + * Get the query statement, based on the path + * + * @return the query statement + */ + public String getQueryStatement() + { + String result = "+TYPE:\"" + ContentModel.TYPE_DICTIONARY_MODEL.toString() + "\""; + if (this.path != null) + { + result += " +PATH:\"" + this.path + "\""; + } + return result; + } + } +} diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrapTest.java b/source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrapTest.java new file mode 100644 index 0000000000..70286a317a --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/DictionaryRepositoryBootstrapTest.java @@ -0,0 +1,241 @@ +package org.alfresco.repo.dictionary; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.DictionaryRepositoryBootstrap.RepositoryLocation; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseAlfrescoSpringTest; + +public class DictionaryRepositoryBootstrapTest extends BaseAlfrescoSpringTest +{ + public static final String TEMPLATE_MODEL_XML = + "" + + + " {1}" + + " Alfresco" + + " 2005-05-30" + + " 1.0" + + + " " + + " " + + " {2} " + + " " + + + " " + + " " + + " " + + + " " + + + " " + + " Base" + + " The Base Type" + + " " + + " " + + " d:text" + + " " + + " " + + " " + + + " " + + + ""; + + /** Behaviour filter */ + private BehaviourFilter behaviourFilter; + + /** The bootstrap service */ + private DictionaryRepositoryBootstrap bootstrap; + + /** The search service */ + private SearchService searchService; + + /** The dictionary DAO */ + private DictionaryDAO dictionaryDAO; + + /** The transaction service */ + private TransactionService transactionService; + + /** The authentication service */ + private AuthenticationComponent authenticationComponent; + + /** + * @see org.springframework.test.AbstractTransactionalSpringContextTests#onSetUpInTransaction() + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the behaviour filter and turn the behaviour off for the model type + this.behaviourFilter = (BehaviourFilter)this.applicationContext.getBean("policyBehaviourFilter"); + this.behaviourFilter.disableBehaviour(ContentModel.TYPE_DICTIONARY_MODEL); + + this.searchService = (SearchService)this.applicationContext.getBean("searchService"); + this.dictionaryDAO = (DictionaryDAO)this.applicationContext.getBean("dictionaryDAO"); + this.transactionService = (TransactionService)this.applicationContext.getBean("transactionComponent"); + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + this.bootstrap = new DictionaryRepositoryBootstrap(); + this.bootstrap.setContentService(this.contentService); + this.bootstrap.setSearchService(this.searchService); + this.bootstrap.setDictionaryDAO(this.dictionaryDAO); + this.bootstrap.setAuthenticationComponent(this.authenticationComponent); + this.bootstrap.setTransactionService(this.transactionService); + + RepositoryLocation location = this.bootstrap.new RepositoryLocation(); + location.setStoreProtocol(this.storeRef.getProtocol()); + location.setStoreId(this.storeRef.getIdentifier()); + // NOTE: we are not setting the path for now .. in doing so we are searching the whole dictionary + + List locations = new ArrayList(); + locations.add(location); + this.bootstrap.setRepositoryLocations(locations); + } + + /** + * Test bootstrap + */ + public void testBootstrap() + { + createModelNode( + "http://www.alfresco.org/model/test2DictionaryBootstrapFromRepo/1.0", + "test2", + "testModel2", + " ", + "Test model two", + "base2", + "prop2"); + createModelNode( + "http://www.alfresco.org/model/test3DictionaryBootstrapFromRepo/1.0", + "test3", + "testModel3", + " ", + "Test model three", + "base3", + "prop3"); + createModelNode( + "http://www.alfresco.org/model/test1DictionaryBootstrapFromRepo/1.0", + "test1", + "testModel1", + "", + "Test model one", + "base1", + "prop1"); + + // Check that the model is not in the dictionary yet + try + { + this.dictionaryDAO.getModel( + QName.createQName("http://www.alfresco.org/model/test1DictionaryBootstrapFromRepo/1.0", "testModel1")); + fail("The model should not be there."); + } + catch (DictionaryException exception) + { + // Ignore since we where expecting this + } + + // Now do the bootstrap + this.bootstrap.bootstrap(); + + // Check that the model is now there + ModelDefinition modelDefinition1 = this.dictionaryDAO.getModel( + QName.createQName("http://www.alfresco.org/model/test1DictionaryBootstrapFromRepo/1.0", "testModel1")); + assertNotNull(modelDefinition1); + ModelDefinition modelDefinition2 = this.dictionaryDAO.getModel( + QName.createQName("http://www.alfresco.org/model/test2DictionaryBootstrapFromRepo/1.0", "testModel2")); + assertNotNull(modelDefinition2); + ModelDefinition modelDefinition3 = this.dictionaryDAO.getModel( + QName.createQName("http://www.alfresco.org/model/test3DictionaryBootstrapFromRepo/1.0", "testModel3")); + assertNotNull(modelDefinition3); + } + + /** + * Create model node + * + * @param uri + * @param prefix + * @param modelLocalName + * @param importStatement + * @param description + * @param typeName + * @param propertyName + * @return + */ + private NodeRef createModelNode( + String uri, + String prefix, + String modelLocalName, + String importStatement, + String description, + String typeName, + String propertyName) + { + // Create a model node + NodeRef model = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}models"), + ContentModel.TYPE_DICTIONARY_MODEL).getChildRef(); + ContentWriter contentWriter1 = this.contentService.getWriter(model, ContentModel.PROP_CONTENT, true); + contentWriter1.setEncoding("UTF-8"); + contentWriter1.setMimetype(MimetypeMap.MIMETYPE_XML); + String modelOne = getModelString( + uri, + prefix, + modelLocalName, + importStatement, + description, + typeName, + propertyName); + contentWriter1.putContent(modelOne); + + return model; + } + + /** + * + * Gets the model string + * + * @param uri + * @param prefix + * @param modelLocalName + * @param importStatement + * @param description + * @param typeName + * @param propertyName + * @return + */ + private String getModelString( + String uri, + String prefix, + String modelLocalName, + String importStatement, + String description, + String typeName, + String propertyName) + { + return MessageFormat.format( + TEMPLATE_MODEL_XML, + new Object[]{ + "'" + prefix +":" + modelLocalName + "'", + description, + importStatement, + "'" + uri + "'", + "'" + prefix + "'", + "'" + prefix + ":" + typeName + "'", + "'" + prefix + ":" + propertyName + "'"}); + } +} diff --git a/source/java/org/alfresco/repo/dictionary/M2AnonymousTypeDefinition.java b/source/java/org/alfresco/repo/dictionary/M2AnonymousTypeDefinition.java new file mode 100644 index 0000000000..ac43ef9e1c --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2AnonymousTypeDefinition.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled anonymous type definition. + * + * @author David Caruana + * + */ +/*package*/ class M2AnonymousTypeDefinition implements TypeDefinition +{ + private TypeDefinition type; + private Map properties = new HashMap(); + private Map associations = new HashMap(); + private Map childassociations = new HashMap(); + + + /** + * Construct + * + * @param type the primary type + * @param aspects the aspects to combine with the type + */ + /*package*/ M2AnonymousTypeDefinition(TypeDefinition type, Collection aspects) + { + this.type = type; + + // Combine features of type and aspects + properties.putAll(type.getProperties()); + associations.putAll(type.getAssociations()); + childassociations.putAll(type.getChildAssociations()); + for (AspectDefinition aspect : aspects) + { + properties.putAll(aspect.getProperties()); + associations.putAll(aspect.getAssociations()); + childassociations.putAll(aspect.getChildAssociations()); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#getModel() + */ + public ModelDefinition getModel() + { + return type.getModel(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.TypeDefinition#getDefaultAspects() + */ + public List getDefaultAspects() + { + return type.getDefaultAspects(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getName() + */ + public QName getName() + { + return QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "anonymous#" + type.getName().getLocalName()); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getTitle() + */ + public String getTitle() + { + return type.getTitle(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getDescription() + */ + public String getDescription() + { + return type.getDescription(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getParentName() + */ + public QName getParentName() + { + return type.getParentName(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#isAspect() + */ + public boolean isAspect() + { + return type.isAspect(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getProperties() + */ + public Map getProperties() + { + return Collections.unmodifiableMap(properties); + } + + /** + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#getDefaultValues() + */ + public Map getDefaultValues() + { + Map result = new HashMap(5); + + for(Map.Entry entry : properties.entrySet()) + { + PropertyDefinition propertyDefinition = entry.getValue(); + String defaultValue = propertyDefinition.getDefaultValue(); + if (defaultValue != null) + { + result.put(entry.getKey(), defaultValue); + } + } + + return Collections.unmodifiableMap(result); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getAssociations() + */ + public Map getAssociations() + { + return Collections.unmodifiableMap(associations); + } + + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#isContainer() + */ + public boolean isContainer() + { + return !childassociations.isEmpty(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getChildAssociations() + */ + public Map getChildAssociations() + { + return Collections.unmodifiableMap(childassociations); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Aspect.java b/source/java/org/alfresco/repo/dictionary/M2Aspect.java new file mode 100644 index 0000000000..d5e86bb3f0 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Aspect.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +/** + * Aspect definition. + * + * @author David Caruana + */ +public class M2Aspect extends M2Class +{ + + /*package*/ M2Aspect() + { + super(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2AspectDefinition.java b/source/java/org/alfresco/repo/dictionary/M2AspectDefinition.java new file mode 100644 index 0000000000..4d84fd4e51 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2AspectDefinition.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled Aspect Definition. + * + * @author David Caruana + */ +/*package*/ class M2AspectDefinition extends M2ClassDefinition + implements AspectDefinition +{ + + /*package*/ M2AspectDefinition(ModelDefinition model, M2Aspect m2Aspect, NamespacePrefixResolver resolver, Map modelProperties, Map modelAssociations) + { + super(model, m2Aspect, resolver, modelProperties, modelAssociations); + } + + @Override + public String getDescription() + { + String value = M2Label.getLabel(model, "aspect", name, "description"); + + // if we don't have a description call the super class + if (value == null) + { + value = super.getDescription(); + } + + return value; + } + + @Override + public String getTitle() + { + String value = M2Label.getLabel(model, "aspect", name, "title"); + + // if we don't have a title call the super class + if (value == null) + { + value = super.getTitle(); + } + + return value; + } +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Association.java b/source/java/org/alfresco/repo/dictionary/M2Association.java new file mode 100644 index 0000000000..dd8744e585 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Association.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Association definition. + * + * @author David Caruana + */ +public class M2Association extends M2ClassAssociation +{ + + /*package*/ M2Association() + { + } + + /*package*/ M2Association(String name) + { + super(name); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java b/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java new file mode 100644 index 0000000000..35c234ef1e --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled Association Definition. + * + * @author David Caruana + */ +/*package*/ class M2AssociationDefinition implements AssociationDefinition +{ + + private ClassDefinition classDef; + private M2ClassAssociation assoc; + private QName name; + private QName targetClassName; + private ClassDefinition targetClass; + private QName sourceRoleName; + private QName targetRoleName; + + + /** + * Construct + * + * @param m2Association association definition + * @return the definition + */ + /*package*/ M2AssociationDefinition(ClassDefinition classDef, M2ClassAssociation assoc, NamespacePrefixResolver resolver) + { + this.classDef = classDef; + this.assoc = assoc; + + // Resolve names + this.name = QName.createQName(assoc.getName(), resolver); + this.targetClassName = QName.createQName(assoc.getTargetClassName(), resolver); + this.sourceRoleName = QName.createQName(assoc.getSourceRoleName(), resolver); + this.targetRoleName = QName.createQName(assoc.getTargetRoleName(), resolver); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(56); + sb.append("Association") + .append("[ class=").append(classDef) + .append(", name=").append(name) + .append(", target class=").append(targetClassName) + .append(", source role=").append(sourceRoleName) + .append(", target role=").append(targetRoleName) + .append("]"); + return sb.toString(); + } + + + /*package*/ M2ClassAssociation getM2Association() + { + return assoc; + } + + + /*package*/ void resolveDependencies(ModelQuery query) + { + if (targetClassName == null) + { + throw new DictionaryException("Target class of association " + name.toPrefixString() + " must be specified"); + } + targetClass = query.getClass(targetClassName); + if (targetClass == null) + { + throw new DictionaryException("Target class " + targetClassName.toPrefixString() + " of association " + name.toPrefixString() + " is not found"); + } + } + + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.AssociationDefinition#getModel() + */ + public ModelDefinition getModel() + { + return classDef.getModel(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getName() + */ + public QName getName() + { + return name; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#isChild() + */ + public boolean isChild() + { + return (assoc instanceof M2ChildAssociation); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getTitle() + */ + public String getTitle() + { + String value = M2Label.getLabel(classDef.getModel(), "association", name, "title"); + if (value == null) + { + value = assoc.getTitle(); + } + return value; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getDescription() + */ + public String getDescription() + { + String value = M2Label.getLabel(classDef.getModel(), "association", name, "description"); + if (value == null) + { + value = assoc.getDescription(); + } + return value; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#isProtected() + */ + public boolean isProtected() + { + return assoc.isProtected(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getSourceClass() + */ + public ClassDefinition getSourceClass() + { + return classDef; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getSourceRoleName() + */ + public QName getSourceRoleName() + { + return sourceRoleName; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#isSourceMandatory() + */ + public boolean isSourceMandatory() + { + return assoc.isSourceMandatory(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#isSourceMany() + */ + public boolean isSourceMany() + { + return assoc.isSourceMany(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getTargetClass() + */ + public ClassDefinition getTargetClass() + { + return targetClass; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#getTargetRoleName() + */ + public QName getTargetRoleName() + { + return targetRoleName; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#isTargetMandatory() + */ + public boolean isTargetMandatory() + { + return assoc.isTargetMandatory(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.AssociationDefinition#isTargetMany() + */ + public boolean isTargetMany() + { + return assoc.isTargetMany(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2ChildAssociation.java b/source/java/org/alfresco/repo/dictionary/M2ChildAssociation.java new file mode 100644 index 0000000000..550138d90b --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2ChildAssociation.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Child Association definition. + * + * @author David Caruana + * + */ +public class M2ChildAssociation extends M2ClassAssociation +{ + private String requiredChildName = null; + private Boolean allowDuplicateChildName = null; + + + /*package*/ M2ChildAssociation() + { + } + + + /*package*/ M2ChildAssociation(String name) + { + super(name); + } + + + public String getRequiredChildName() + { + return requiredChildName; + } + + + public void setRequiredChildName(String requiredChildName) + { + this.requiredChildName = requiredChildName; + } + + + public boolean allowDuplicateChildName() + { + return allowDuplicateChildName == null ? true : allowDuplicateChildName; + } + + + public void setAllowDuplicateChildName(boolean allowDuplicateChildName) + { + this.allowDuplicateChildName = allowDuplicateChildName; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2ChildAssociationDefinition.java b/source/java/org/alfresco/repo/dictionary/M2ChildAssociationDefinition.java new file mode 100644 index 0000000000..2b95c01b45 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2ChildAssociationDefinition.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; + + +/** + * Compiled Association Definition. + * + * @author David Caruana + */ +/*package*/ class M2ChildAssociationDefinition extends M2AssociationDefinition + implements ChildAssociationDefinition +{ + + /** + * Construct + * @param classDef class definition + * @param assoc child assocation + * @param resolver namespace resolver + */ + /*package*/ M2ChildAssociationDefinition(ClassDefinition classDef, M2ChildAssociation assoc, NamespacePrefixResolver resolver) + { + super(classDef, assoc, resolver); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ChildAssociationDefinition#getRequiredChildName() + */ + public String getRequiredChildName() + { + return ((M2ChildAssociation)getM2Association()).getRequiredChildName(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ChildAssociationDefinition#getDuplicateChildNamesAllowed() + */ + public boolean getDuplicateChildNamesAllowed() + { + return ((M2ChildAssociation)getM2Association()).allowDuplicateChildName(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Class.java b/source/java/org/alfresco/repo/dictionary/M2Class.java new file mode 100644 index 0000000000..1701d6e70d --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Class.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Abstract Class Definition. + * + * @author David Caruana + * + */ +public abstract class M2Class +{ + private String name = null; + private String title = null; + private String description = null; + private String parentName = null; + + private List properties = new ArrayList(); + private List propertyOverrides = new ArrayList(); + private List associations = new ArrayList(); + private List mandatoryAspects = new ArrayList(); + + /*package*/ M2Class() + { + } + + + public String getName() + { + return name; + } + + + public void setName(String name) + { + this.name = name; + } + + + public String getTitle() + { + return title; + } + + + public void setTitle(String title) + { + this.title = title; + } + + + public String getDescription() + { + return description; + } + + + public void setDescription(String description) + { + this.description = description; + } + + + public String getParentName() + { + return parentName; + } + + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + + public M2Property createProperty(String name) + { + M2Property property = new M2Property(); + property.setName(name); + properties.add(property); + return property; + } + + + public void removeProperty(String name) + { + M2Property property = getProperty(name); + if (property != null) + { + properties.remove(property); + } + } + + + public List getProperties() + { + return Collections.unmodifiableList(properties); + } + + + public M2Property getProperty(String name) + { + for (M2Property candidate : properties) + { + if (candidate.getName().equals(name)) + { + return candidate; + } + } + return null; + } + + + public M2Association createAssociation(String name) + { + M2Association association = new M2Association(); + association.setName(name); + associations.add(association); + return association; + } + + + public M2ChildAssociation createChildAssociation(String name) + { + M2ChildAssociation association = new M2ChildAssociation(); + association.setName(name); + associations.add(association); + return association; + } + + + public void removeAssociation(String name) + { + M2ClassAssociation association = getAssociation(name); + if (association != null) + { + associations.remove(association); + } + } + + + public List getAssociations() + { + return Collections.unmodifiableList(associations); + } + + + public M2ClassAssociation getAssociation(String name) + { + for (M2ClassAssociation candidate : associations) + { + if (candidate.getName().equals(name)) + { + return candidate; + } + } + return null; + } + + + public M2PropertyOverride createPropertyOverride(String name) + { + M2PropertyOverride property = new M2PropertyOverride(); + property.setName(name); + propertyOverrides.add(property); + return property; + } + + + public void removePropertyOverride(String name) + { + M2PropertyOverride property = getPropertyOverride(name); + if (property != null) + { + propertyOverrides.remove(property); + } + } + + + public List getPropertyOverrides() + { + return Collections.unmodifiableList(propertyOverrides); + } + + + public M2PropertyOverride getPropertyOverride(String name) + { + for (M2PropertyOverride candidate : propertyOverrides) + { + if (candidate.getName().equals(name)) + { + return candidate; + } + } + return null; + } + + public void addMandatoryAspect(String name) + { + mandatoryAspects.add(name); + } + + + public void removeMandatoryAspect(String name) + { + mandatoryAspects.remove(name); + } + + + public List getMandatoryAspects() + { + return Collections.unmodifiableList(mandatoryAspects); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java b/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java new file mode 100644 index 0000000000..1d35b8f52a --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Abstract Association Definition. + * + * @author David Caruana + * + */ +public abstract class M2ClassAssociation +{ + private String name = null; + private Boolean isProtected = null; + private String title = null; + private String description = null; + private String sourceRoleName = null; + private Boolean isSourceMandatory = null; + private Boolean isSourceMany = null; + private String targetClassName = null; + private String targetRoleName = null; + private Boolean isTargetMandatory = null; + private Boolean isTargetMany = null; + + + /*package*/ M2ClassAssociation() + { + } + + + /*package*/ M2ClassAssociation(String name) + { + this.name = name; + } + + + public boolean isChild() + { + return this instanceof M2ChildAssociation; + } + + + public String getName() + { + return name; + } + + + public void setName(String name) + { + this.name = name; + } + + + public boolean isProtected() + { + return isProtected == null ? false : isProtected; + } + + + public void setProtected(boolean isProtected) + { + this.isProtected = isProtected; + } + + + public String getTitle() + { + return title; + } + + + public void setTitle(String title) + { + this.title = title; + } + + + public String getDescription() + { + return description; + } + + + public void setDescription(String description) + { + this.description = description; + } + + + public String getSourceRoleName() + { + return sourceRoleName; + } + + + public void setSourceRoleName(String name) + { + this.sourceRoleName = name; + } + + + public boolean isSourceMandatory() + { + return isSourceMandatory == null ? true : isSourceMandatory; + } + + + public void setSourceMandatory(boolean isSourceMandatory) + { + this.isSourceMandatory = isSourceMandatory; + } + + + public boolean isSourceMany() + { + return isSourceMany == null ? false : isSourceMany; + } + + + public void setSourceMany(boolean isSourceMany) + { + this.isSourceMany = isSourceMany; + } + + + public String getTargetClassName() + { + return targetClassName; + } + + + public void setTargetClassName(String targetClassName) + { + this.targetClassName = targetClassName; + } + + + public String getTargetRoleName() + { + return targetRoleName; + } + + + public void setTargetRoleName(String name) + { + this.targetRoleName = name; + } + + + public boolean isTargetMandatory() + { + return isTargetMandatory == null ? false : isTargetMandatory; + } + + + public void setTargetMandatory(boolean isTargetMandatory) + { + this.isTargetMandatory = isTargetMandatory; + } + + + public boolean isTargetMany() + { + return isTargetMany == null ? true : isTargetMany; + } + + + public void setTargetMany(boolean isTargetMany) + { + this.isTargetMany = isTargetMany; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2ClassDefinition.java b/source/java/org/alfresco/repo/dictionary/M2ClassDefinition.java new file mode 100644 index 0000000000..da034c42d4 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2ClassDefinition.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled Class Definition + * + * @author David Caruana + */ +/*package*/ class M2ClassDefinition implements ClassDefinition +{ + protected ModelDefinition model; + protected M2Class m2Class; + protected QName name; + protected QName parentName = null; + + private Map propertyOverrides = new HashMap(); + private Map properties = new HashMap(); + private Map inheritedProperties = new HashMap(); + private Map associations = new HashMap(); + private Map inheritedAssociations = new HashMap(); + private Map inheritedChildAssociations = new HashMap(); + private List defaultAspects = new ArrayList(); + private List defaultAspectNames = new ArrayList(); + private List inheritedDefaultAspects = new ArrayList(); + + + /** + * Construct + * + * @param m2Class class definition + * @param resolver namepsace resolver + * @param modelProperties global list of model properties + * @param modelAssociations global list of model associations + */ + /*package*/ M2ClassDefinition(ModelDefinition model, M2Class m2Class, NamespacePrefixResolver resolver, Map modelProperties, Map modelAssociations) + { + this.model = model; + this.m2Class = m2Class; + + // Resolve Names + this.name = QName.createQName(m2Class.getName(), resolver); + if (m2Class.getParentName() != null && m2Class.getParentName().length() > 0) + { + this.parentName = QName.createQName(m2Class.getParentName(), resolver); + } + + // Construct Properties + for (M2Property property : m2Class.getProperties()) + { + PropertyDefinition def = new M2PropertyDefinition(this, property, resolver); + if (properties.containsKey(def.getName())) + { + throw new DictionaryException("Found duplicate property definition " + def.getName().toPrefixString() + " within class " + name.toPrefixString()); + } + + // Check for existence of property elsewhere within the model + PropertyDefinition existingDef = modelProperties.get(def.getName()); + if (existingDef != null) + { + // TODO: Consider sharing property, if property definitions are equal + throw new DictionaryException("Found duplicate property definition " + def.getName().toPrefixString() + " within class " + + name.toPrefixString() + " and class " + existingDef.getContainerClass().getName().toPrefixString()); + } + + properties.put(def.getName(), def); + modelProperties.put(def.getName(), def); + } + + // Construct Associations + for (M2ClassAssociation assoc : m2Class.getAssociations()) + { + AssociationDefinition def; + if (assoc instanceof M2ChildAssociation) + { + def = new M2ChildAssociationDefinition(this, (M2ChildAssociation)assoc, resolver); + } + else + { + def = new M2AssociationDefinition(this, assoc, resolver); + } + if (associations.containsKey(def.getName())) + { + throw new DictionaryException("Found duplicate association definition " + def.getName().toPrefixString() + " within class " + name.toPrefixString()); + } + + // Check for existence of association elsewhere within the model + AssociationDefinition existingDef = modelAssociations.get(def.getName()); + if (existingDef != null) + { + // TODO: Consider sharing association, if association definitions are equal + throw new DictionaryException("Found duplicate association definition " + def.getName().toPrefixString() + " within class " + + name.toPrefixString() + " and class " + existingDef.getSourceClass().getName().toPrefixString()); + } + + associations.put(def.getName(), def); + modelAssociations.put(def.getName(), def); + } + + // Construct Property overrides + for (M2PropertyOverride override : m2Class.getPropertyOverrides()) + { + QName overrideName = QName.createQName(override.getName(), resolver); + if (properties.containsKey(overrideName)) + { + throw new DictionaryException("Found duplicate property and property override definition " + overrideName.toPrefixString() + " within class " + name.toPrefixString()); + } + if (propertyOverrides.containsKey(overrideName)) + { + throw new DictionaryException("Found duplicate property override definition " + overrideName.toPrefixString() + " within class " + name.toPrefixString()); + } + propertyOverrides.put(overrideName, override); + } + + // Resolve qualified names + for (String aspectName : m2Class.getMandatoryAspects()) + { + QName name = QName.createQName(aspectName, resolver); + if (!defaultAspectNames.contains(name)) + { + defaultAspectNames.add(name); + } + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(120); + sb.append("ClassDef ") + .append("[ name=").append(name) + .append("]"); + return sb.toString(); + } + + + /*package*/ void resolveDependencies(ModelQuery query) + { + if (parentName != null) + { + ClassDefinition parent = query.getClass(parentName); + if (parent == null) + { + throw new DictionaryException("Parent class " + parentName.toPrefixString() + " of class " + name.toPrefixString() + " is not found"); + } + } + + for (PropertyDefinition def : properties.values()) + { + ((M2PropertyDefinition)def).resolveDependencies(query); + } + for (AssociationDefinition def : associations.values()) + { + ((M2AssociationDefinition)def).resolveDependencies(query); + } + + for (QName aspectName : defaultAspectNames) + { + AspectDefinition aspect = query.getAspect(aspectName); + if (aspect == null) + { + throw new DictionaryException("Mandatory aspect " + aspectName.toPrefixString() + " of class " + name.toPrefixString() + " is not found"); + } + defaultAspects.add(aspect); + } + } + + + /*package*/ void resolveInheritance(ModelQuery query) + { + // Retrieve parent class + ClassDefinition parentClass = (parentName == null) ? null : query.getClass(parentName); + + // Build list of inherited properties (and process overridden values) + if (parentClass != null) + { + for (PropertyDefinition def : parentClass.getProperties().values()) + { + M2PropertyOverride override = propertyOverrides.get(def.getName()); + if (override == null) + { + inheritedProperties.put(def.getName(), def); + } + else + { + inheritedProperties.put(def.getName(), new M2PropertyDefinition(this, def, override)); + } + } + } + + // Append list of defined properties + for (PropertyDefinition def : properties.values()) + { + if (inheritedProperties.containsKey(def.getName())) + { + throw new DictionaryException("Duplicate property definition " + def.getName().toPrefixString() + " found in class hierarchy of " + name.toPrefixString()); + } + inheritedProperties.put(def.getName(), def); + } + + // Build list of inherited associations + if (parentClass != null) + { + inheritedAssociations.putAll(parentClass.getAssociations()); + } + + // Append list of defined associations + for (AssociationDefinition def : associations.values()) + { + if (inheritedAssociations.containsKey(def.getName())) + { + throw new DictionaryException("Duplicate association definition " + def.getName().toPrefixString() + " found in class hierarchy of " + name.toPrefixString()); + } + inheritedAssociations.put(def.getName(), def); + } + + // Derive Child Associations + for (AssociationDefinition def : inheritedAssociations.values()) + { + if (def instanceof ChildAssociationDefinition) + { + inheritedChildAssociations.put(def.getName(), (ChildAssociationDefinition)def); + } + } + + // Build list of inherited default aspects + if (parentClass != null) + { + inheritedDefaultAspects.addAll(parentClass.getDefaultAspects()); + } + + // Append list of defined default aspects + for (AspectDefinition def : defaultAspects) + { + if (!inheritedDefaultAspects.contains(def)) + { + inheritedDefaultAspects.add(def); + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#getModel() + */ + public ModelDefinition getModel() + { + return model; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getName() + */ + public QName getName() + { + return name; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getTitle() + */ + public String getTitle() + { + String value = M2Label.getLabel(model, "class", name, "title"); + if (value == null) + { + value = m2Class.getTitle(); + } + return value; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getDescription() + */ + public String getDescription() + { + String value = M2Label.getLabel(model, "class", name, "description"); + if (value == null) + { + value = m2Class.getDescription(); + } + return value; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#isAspect() + */ + public boolean isAspect() + { + return (m2Class instanceof M2Aspect); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getParentName() + */ + public QName getParentName() + { + return parentName; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getProperties() + */ + public Map getProperties() + { + return Collections.unmodifiableMap(inheritedProperties); + } + + /** + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#getDefaultValues() + */ + public Map getDefaultValues() + { + Map result = new HashMap(5); + + for(Map.Entry entry : inheritedProperties.entrySet()) + { + PropertyDefinition propertyDefinition = entry.getValue(); + String defaultValue = propertyDefinition.getDefaultValue(); + if (defaultValue != null) + { + result.put(entry.getKey(), defaultValue); + } + } + + return Collections.unmodifiableMap(result); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getAssociations() + */ + public Map getAssociations() + { + return Collections.unmodifiableMap(inheritedAssociations); + } + + /** + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#getDefaultAspects() + */ + public List getDefaultAspects() + { + return inheritedDefaultAspects; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#isContainer() + */ + public boolean isContainer() + { + return !inheritedChildAssociations.isEmpty(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ClassDefinition#getChildAssociations() + */ + public Map getChildAssociations() + { + return Collections.unmodifiableMap(inheritedChildAssociations); + } + + @Override + public int hashCode() + { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof M2ClassDefinition)) + { + return false; + } + return name.equals(((M2ClassDefinition)obj).name); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2DataType.java b/source/java/org/alfresco/repo/dictionary/M2DataType.java new file mode 100644 index 0000000000..eca83e6644 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2DataType.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Property Type Definition + * + * @author David Caruana + * + */ +public class M2DataType +{ + private String name = null; + private String title = null; + private String description = null; + private String analyserClassName = null; + private String javaClassName = null; + + + /*package*/ M2DataType() + { + super(); + } + + + public String getName() + { + return name; + } + + + public void setName(String name) + { + this.name = name; + } + + + public String getTitle() + { + return title; + } + + + public void setTitle(String title) + { + this.title = title; + } + + + public String getDescription() + { + return description; + } + + + public void setDescription(String description) + { + this.description = description; + } + + + public String getAnalyserClassName() + { + return analyserClassName; + } + + + public void setAnalyserClassName(String analyserClassName) + { + this.analyserClassName = analyserClassName;; + } + + + public String getJavaClassName() + { + return javaClassName; + } + + + public void setJavaClassName(String javaClassName) + { + this.javaClassName = javaClassName;; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2DataTypeDefinition.java b/source/java/org/alfresco/repo/dictionary/M2DataTypeDefinition.java new file mode 100644 index 0000000000..35f4d96988 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2DataTypeDefinition.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Locale; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled Property Type Definition + * + * @author David Caruana + * + */ +/*package*/ class M2DataTypeDefinition implements DataTypeDefinition +{ + private ModelDefinition model; + private QName name; + private M2DataType dataType; + + + /*package*/ M2DataTypeDefinition(ModelDefinition model, M2DataType propertyType, NamespacePrefixResolver resolver) + { + this.model = model; + this.name = QName.createQName(propertyType.getName(), resolver); + this.dataType = propertyType; + } + + + /*package*/ void resolveDependencies(ModelQuery query) + { + // Ensure java class has been specified + String javaClass = dataType.getJavaClassName(); + if (javaClass == null) + { + throw new DictionaryException("Java class of data type " + name.toPrefixString() + " must be specified"); + } + + // Ensure java class is valid and referenceable + try + { + Class.forName(javaClass); + } + catch (ClassNotFoundException e) + { + throw new DictionaryException("Java class " + javaClass + " of data type " + name.toPrefixString() + " is invalid", e); + } + } + + /** + * @see #getName() + */ + public String toString() + { + return getName().toString(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.DataTypeDefinition#getModel() + */ + public ModelDefinition getModel() + { + return model; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyTypeDefinition#getName() + */ + public QName getName() + { + return name; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyTypeDefinition#getTitle() + */ + public String getTitle() + { + String value = M2Label.getLabel(model, "datatype", name, "title"); + if (value == null) + { + value = dataType.getTitle(); + } + return value; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyTypeDefinition#getDescription() + */ + public String getDescription() + { + String value = M2Label.getLabel(model, "datatype", name, "description"); + if (value == null) + { + value = dataType.getDescription(); + } + return value; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyTypeDefinition#getAnalyserClassName() + */ + public String getAnalyserClassName() + { + return getAnalyserClassName(I18NUtil.getLocale()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.DataTypeDefinition#getAnalyserClassName(java.util.Locale) + */ + public String getAnalyserClassName(Locale locale) + { + String value = M2Label.getLabel(locale, model, "datatype", name, "analyzer"); + if (value == null) + { + value = dataType.getAnalyserClassName(); + } + return value; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.PropertyTypeDefinition#getJavaClassName() + */ + public String getJavaClassName() + { + return dataType.getJavaClassName(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Label.java b/source/java/org/alfresco/repo/dictionary/M2Label.java new file mode 100644 index 0000000000..a732d1c574 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Label.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Locale; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.util.StringUtils; + + +/** + * Helper for obtaining display labels for data dictionary items + * + * @author David Caruana + */ +public class M2Label +{ + + /** + * Get label for data dictionary item given specified locale + * + * @param locale + * @param model + * @param type + * @param item + * @param label + * @return + */ + public static String getLabel(Locale locale, ModelDefinition model, String type, QName item, String label) + { + String key = model.getName().toPrefixString(); + if (type != null) + { + key += "." + type; + } + if (item != null) + { + key += "." + item.toPrefixString(); + } + key += "." + label; + key = StringUtils.replace(key, ":", "_"); + return I18NUtil.getMessage(key, locale); + } + + /** + * Get label for data dictionary item + * + * @param model + * @param type + * @param item + * @param label + * @return + */ + public static String getLabel(ModelDefinition model, String type, QName item, String label) + { + return getLabel(I18NUtil.getLocale(), model, type, item, label); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Model.java b/source/java/org/alfresco/repo/dictionary/M2Model.java new file mode 100644 index 0000000000..384c534a1f --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Model.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.jibx.runtime.BindingDirectory; +import org.jibx.runtime.IBindingFactory; +import org.jibx.runtime.IMarshallingContext; +import org.jibx.runtime.IUnmarshallingContext; +import org.jibx.runtime.JiBXException; + + +/** + * Model Definition. + * + * @author David Caruana + * + */ +public class M2Model +{ + private String name = null; + private String description = null; + private String author = null; + private Date published = null; + private String version; + + private List namespaces = new ArrayList(); + private List imports = new ArrayList(); + private List dataTypes = new ArrayList(); + private List types = new ArrayList(); + private List aspects = new ArrayList(); + + + private M2Model() + { + } + + + /** + * Construct an empty model + * + * @param name the name of the model + * @return the model + */ + public static M2Model createModel(String name) + { + M2Model model = new M2Model(); + model.name = name; + return model; + } + + + /** + * Construct a model from a dictionary xml specification + * + * @param xml the dictionary xml + * @return the model representation of the xml + */ + public static M2Model createModel(InputStream xml) + { + try + { + IBindingFactory factory = BindingDirectory.getFactory(M2Model.class); + IUnmarshallingContext context = factory.createUnmarshallingContext(); + Object obj = context.unmarshalDocument(xml, null); + return (M2Model)obj; + } + catch(JiBXException e) + { + throw new DictionaryException("Failed to parse model", e); + } + } + + + /** + * Render the model to dictionary XML + * + * @param xml the dictionary xml representation of the model + */ + public void toXML(OutputStream xml) + { + try + { + IBindingFactory factory = BindingDirectory.getFactory(M2Model.class); + IMarshallingContext context = factory.createMarshallingContext(); + context.setIndent(4); + context.marshalDocument(this, "UTF-8", null, xml); + } + catch(JiBXException e) + { + throw new DictionaryException("Failed to create M2 Model", e); + } + } + + + /** + * Create a compiled form of this model + * + * @param dictionaryDAO dictionary DAO + * @param namespaceDAO namespace DAO + * @return the compiled form of the model + */ + /*package*/ CompiledModel compile(DictionaryDAO dictionaryDAO, NamespaceDAO namespaceDAO) + { + CompiledModel compiledModel = new CompiledModel(this, dictionaryDAO, namespaceDAO); + return compiledModel; + } + + + public String getName() + { + return name; + } + + + public void setName(String name) + { + this.name = name; + } + + + public String getDescription() + { + return description; + } + + + public void setDescription(String description) + { + this.description = description; + } + + + public String getAuthor() + { + return author; + } + + + public void setAuthor(String author) + { + this.author = author; + } + + + public Date getPublishedDate() + { + return published; + } + + + public void setPublishedDate(Date published) + { + this.published = published; + } + + + public String getVersion() + { + return version; + } + + + public void setVersion(String version) + { + this.version = version; + } + + + public M2Type createType(String name) + { + M2Type type = new M2Type(); + type.setName(name); + types.add(type); + return type; + } + + + public void removeType(String name) + { + M2Type type = getType(name); + if (type != null) + { + types.remove(types); + } + } + + + public List getTypes() + { + return Collections.unmodifiableList(types); + } + + + public M2Type getType(String name) + { + for (M2Type candidate : types) + { + if (candidate.getName().equals(name)) + { + return candidate; + } + } + return null; + } + + + public M2Aspect createAspect(String name) + { + M2Aspect aspect = new M2Aspect(); + aspect.setName(name); + aspects.add(aspect); + return aspect; + } + + + public void removeAspect(String name) + { + M2Aspect aspect = getAspect(name); + if (aspect != null) + { + aspects.remove(name); + } + } + + + public List getAspects() + { + return Collections.unmodifiableList(aspects); + } + + + public M2Aspect getAspect(String name) + { + for (M2Aspect candidate : aspects) + { + if (candidate.getName().equals(name)) + { + return candidate; + } + } + return null; + } + + + public M2DataType createPropertyType(String name) + { + M2DataType type = new M2DataType(); + type .setName(name); + dataTypes.add(type); + return type; + } + + + public void removePropertyType(String name) + { + M2DataType type = getPropertyType(name); + if (type != null) + { + dataTypes.remove(name); + } + } + + + public List getPropertyTypes() + { + return Collections.unmodifiableList(dataTypes); + } + + + public M2DataType getPropertyType(String name) + { + for (M2DataType candidate : dataTypes) + { + if (candidate.getName().equals(name)) + { + return candidate; + } + } + return null; + } + + + public M2Namespace createNamespace(String uri, String prefix) + { + M2Namespace namespace = new M2Namespace(); + namespace.setUri(uri); + namespace.setPrefix(prefix); + namespaces.add(namespace); + return namespace; + } + + + public void removeNamespace(String uri) + { + M2Namespace namespace = getNamespace(uri); + if (namespace != null) + { + namespaces.remove(namespace); + } + } + + + public List getNamespaces() + { + return Collections.unmodifiableList(namespaces); + } + + + public M2Namespace getNamespace(String uri) + { + for (M2Namespace candidate : namespaces) + { + if (candidate.getUri().equals(uri)) + { + return candidate; + } + } + return null; + } + + + public M2Namespace createImport(String uri, String prefix) + { + M2Namespace namespace = new M2Namespace(); + namespace.setUri(uri); + namespace.setPrefix(prefix); + imports.add(namespace); + return namespace; + } + + + public void removeImport(String uri) + { + M2Namespace namespace = getImport(uri); + if (namespace != null) + { + imports.remove(namespace); + } + } + + + public List getImports() + { + return Collections.unmodifiableList(imports); + } + + + public M2Namespace getImport(String uri) + { + for (M2Namespace candidate : imports) + { + if (candidate.getUri().equals(uri)) + { + return candidate; + } + } + return null; + } + + + // Do not delete: referenced by m2binding.xml + private static List createList() + { + return new ArrayList(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2ModelDefinition.java b/source/java/org/alfresco/repo/dictionary/M2ModelDefinition.java new file mode 100644 index 0000000000..97ef4e3d9e --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2ModelDefinition.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Date; + +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + +/** + * Compiled Model Definition + * + * @author David Caruana + * + */ +public class M2ModelDefinition implements ModelDefinition +{ + private QName name; + private M2Model model; + + + /*package*/ M2ModelDefinition(M2Model model, NamespacePrefixResolver resolver) + { + this.name = QName.createQName(model.getName(), resolver); + this.model = model; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelDefinition#getName() + */ + public QName getName() + { + return name; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelDefinition#getDescription() + */ + public String getDescription() + { + String value = M2Label.getLabel(this, null, null, "description"); + if (value == null) + { + value = model.getDescription(); + } + return value; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelDefinition#getAuthor() + */ + public String getAuthor() + { + return model.getAuthor(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelDefinition#getPublishedDate() + */ + public Date getPublishedDate() + { + return model.getPublishedDate(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.ModelDefinition#getVersion() + */ + public String getVersion() + { + return model.getVersion(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Namespace.java b/source/java/org/alfresco/repo/dictionary/M2Namespace.java new file mode 100644 index 0000000000..40ce392173 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Namespace.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Namespace Definition. + * + * @author David Caruana + * + */ +public class M2Namespace +{ + private String uri = null; + private String prefix = null; + + + /*package*/ M2Namespace() + { + } + + + public String getUri() + { + return uri; + } + + + public void setUri(String uri) + { + this.uri = uri; + } + + + public String getPrefix() + { + return prefix; + } + + + public void setPrefix(String prefix) + { + this.prefix = prefix; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Property.java b/source/java/org/alfresco/repo/dictionary/M2Property.java new file mode 100644 index 0000000000..55db4978ba --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Property.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Property Definition + * + * @author David Caruana + * + */ +public class M2Property +{ + private String name = null; + private String title = null; + private String description = null; + private String propertyType = null; + private boolean isProtected = false; + private boolean isMandatory = false; + private boolean isMultiValued = false; + private String defaultValue = null; + private boolean isIndexed = true; + private boolean isIndexedAtomically = true; + private boolean isStoredInIndex = false; + private boolean isTokenisedInIndex = true; + + + /*package*/ M2Property() + { + } + + + /*package*/ M2Property(String name) + { + this.name = name; + } + + + public String getName() + { + return name; + } + + + public void setName(String name) + { + this.name = name; + } + + + public String getTitle() + { + return title; + } + + + public void setTitle(String title) + { + this.title = title; + } + + + public String getDescription() + { + return description; + } + + + public void setDescription(String description) + { + this.description = description; + } + + + public String getType() + { + return propertyType; + } + + + public void setType(String type) + { + this.propertyType = type; + } + + + public boolean isProtected() + { + return isProtected; + } + + + public void setProtected(boolean isProtected) + { + this.isProtected = isProtected; + } + + + public boolean isMandatory() + { + return isMandatory; + } + + + public void setMandatory(boolean isMandatory) + { + this.isMandatory = isMandatory; + } + + + public boolean isMultiValued() + { + return isMultiValued; + } + + + public void setMultiValued(boolean isMultiValued) + { + this.isMultiValued = isMultiValued; + } + + + public String getDefaultValue() + { + return defaultValue; + } + + + public void setDefaultValue(String defaultValue) + { + this.defaultValue = defaultValue; + } + + + public boolean isIndexed() + { + return isIndexed; + } + + + public void setIndexed(boolean isIndexed) + { + this.isIndexed = isIndexed; + } + + + public boolean isStoredInIndex() + { + return isStoredInIndex; + } + + + public void setStoredInIndex(boolean isStoredInIndex) + { + this.isStoredInIndex = isStoredInIndex; + } + + + public boolean isIndexedAtomically() + { + return isIndexedAtomically; + } + + + public void setIndexedAtomically(boolean isIndexedAtomically) + { + this.isIndexedAtomically = isIndexedAtomically; + } + + + public boolean isTokenisedInIndex() + { + return isTokenisedInIndex; + } + + + public void setTokenisedInIndex(boolean isTokenisedInIndex) + { + this.isTokenisedInIndex = isTokenisedInIndex; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2PropertyDefinition.java b/source/java/org/alfresco/repo/dictionary/M2PropertyDefinition.java new file mode 100644 index 0000000000..d6f2f713aa --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2PropertyDefinition.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled Property Definition + * + * @author David Caruana + */ +/*package*/ class M2PropertyDefinition implements PropertyDefinition +{ + private ClassDefinition classDef; + private M2Property property; + private QName name; + private QName propertyTypeName; + private DataTypeDefinition dataType; + + + /*package*/ M2PropertyDefinition(ClassDefinition classDef, M2Property m2Property, NamespacePrefixResolver resolver) + { + this.classDef = classDef; + this.property = m2Property; + + // Resolve Names + this.name = QName.createQName(property.getName(), resolver); + this.propertyTypeName = QName.createQName(property.getType(), resolver); + } + + + /*package*/ M2PropertyDefinition(ClassDefinition classDef, PropertyDefinition propertyDef, M2PropertyOverride override) + { + this.classDef = classDef; + this.property = createOverriddenProperty(propertyDef, override); + this.name = propertyDef.getName(); + this.dataType = propertyDef.getDataType(); + this.propertyTypeName = this.dataType.getName(); + } + + + /*package*/ void resolveDependencies(ModelQuery query) + { + if (propertyTypeName == null) + { + throw new DictionaryException("Property type of property " + name.toPrefixString() + " must be specified"); + } + dataType = query.getDataType(propertyTypeName); + if (dataType == null) + { + throw new DictionaryException("Property type " + propertyTypeName.toPrefixString() + " of property " + name.toPrefixString() + " is not found"); + } + + // ensure content properties are not multi-valued + if (propertyTypeName.equals(DataTypeDefinition.CONTENT) && isMultiValued()) + { + throw new DictionaryException("Content properties must be single-valued"); + } + } + + + /** + * Create a property definition whose values are overridden + * + * @param propertyDef the property definition to override + * @param override the overridden values + * @return the property definition + */ + private M2Property createOverriddenProperty(PropertyDefinition propertyDef, M2PropertyOverride override) + { + M2Property property = new M2Property(); + + // Process Default Value + String defaultValue = override.getDefaultValue(); + property.setDefaultValue(defaultValue == null ? propertyDef.getDefaultValue() : defaultValue); + + // Process Mandatory Value + Boolean isMandatory = override.isMandatory(); + if (isMandatory != null) + { + if (propertyDef.isMandatory() == true && isMandatory == false) + { + throw new DictionaryException("Cannot relax mandatory attribute of property " + propertyDef.getName().toPrefixString()); + } + } + property.setMandatory(isMandatory == null ? propertyDef.isMandatory() : isMandatory); + + // Copy all other properties as they are + property.setDescription(propertyDef.getDescription()); + property.setIndexed(propertyDef.isIndexed()); + property.setIndexedAtomically(propertyDef.isIndexedAtomically()); + property.setMultiValued(propertyDef.isMultiValued()); + property.setProtected(propertyDef.isProtected()); + property.setStoredInIndex(propertyDef.isStoredInIndex()); + property.setTitle(propertyDef.getTitle()); + property.setTokenisedInIndex(propertyDef.isTokenisedInIndex()); + + return property; + } + + /** + * @see #getName() + */ + @Override + public String toString() + { + return getName().toString(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.PropertyDefinition#getModel() + */ + public ModelDefinition getModel() + { + return classDef.getModel(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#getName() + */ + public QName getName() + { + return name; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#getTitle() + */ + public String getTitle() + { + String value = M2Label.getLabel(classDef.getModel(), "property", name, "title"); + if (value == null) + { + value = property.getTitle(); + } + return value; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#getDescription() + */ + public String getDescription() + { + String value = M2Label.getLabel(classDef.getModel(), "property", name, "description"); + if (value == null) + { + value = property.getDescription(); + } + return value; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#getDefaultValue() + */ + public String getDefaultValue() + { + return property.getDefaultValue(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#getPropertyType() + */ + public DataTypeDefinition getDataType() + { + return dataType; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#getContainerClass() + */ + public ClassDefinition getContainerClass() + { + return classDef; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isMultiValued() + */ + public boolean isMultiValued() + { + return property.isMultiValued(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isMandatory() + */ + public boolean isMandatory() + { + return property.isMandatory(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isProtected() + */ + public boolean isProtected() + { + return property.isProtected(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isIndexed() + */ + public boolean isIndexed() + { + return property.isIndexed(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isStoredInIndex() + */ + public boolean isStoredInIndex() + { + return property.isStoredInIndex(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isIndexedAtomically() + */ + public boolean isIndexedAtomically() + { + return property.isIndexedAtomically(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.PropertyDefinition#isTokenisedInIndex() + */ + public boolean isTokenisedInIndex() + { + return property.isTokenisedInIndex(); + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2PropertyOverride.java b/source/java/org/alfresco/repo/dictionary/M2PropertyOverride.java new file mode 100644 index 0000000000..e87cf77dae --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2PropertyOverride.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Property override definition + * + * @author David Caruana + * + */ +public class M2PropertyOverride +{ + private String name; + private Boolean isMandatory; + private String defaultValue; + + + /*package*/ M2PropertyOverride() + { + } + + + public String getName() + { + return name; + } + + + public void setName(String name) + { + this.name = name; + } + + + public Boolean isMandatory() + { + return isMandatory; + } + + + public void setMandatory(Boolean isMandatory) + { + this.isMandatory = isMandatory; + } + + + public String getDefaultValue() + { + return defaultValue; + } + + + public void setDefaultValue(String defaultValue) + { + this.defaultValue = defaultValue; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/M2Type.java b/source/java/org/alfresco/repo/dictionary/M2Type.java new file mode 100644 index 0000000000..405b302eba --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2Type.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + + +/** + * Type Definition + * + * @author David Caruana + * + */ +public class M2Type extends M2Class +{ + /*package*/ M2Type() + { + super(); + } +} diff --git a/source/java/org/alfresco/repo/dictionary/M2TypeDefinition.java b/source/java/org/alfresco/repo/dictionary/M2TypeDefinition.java new file mode 100644 index 0000000000..f23c85a82e --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2TypeDefinition.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + + +/** + * Compiled Type Definition + * + * @author David Caruana + */ +/*package*/ class M2TypeDefinition extends M2ClassDefinition + implements TypeDefinition +{ + /*package*/ M2TypeDefinition(ModelDefinition model, M2Type m2Type, NamespacePrefixResolver resolver, Map modelProperties, Map modelAssociations) + { + super(model, m2Type, resolver, modelProperties, modelAssociations); + } + + @Override + public String getDescription() + { + String value = M2Label.getLabel(model, "type", name, "description"); + + // if we don't have a description call the super class + if (value == null) + { + value = super.getDescription(); + } + + return value; + } + + @Override + public String getTitle() + { + String value = M2Label.getLabel(model, "type", name, "title"); + + // if we don't have a title call the super class + if (value == null) + { + value = super.getTitle(); + } + + return value; + } +} diff --git a/source/java/org/alfresco/repo/dictionary/M2XML.java b/source/java/org/alfresco/repo/dictionary/M2XML.java new file mode 100644 index 0000000000..32e5655cee --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/M2XML.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.alfresco.util.CachingDateFormat; + + +/** + * Support translating model from and to XML + * + * @author David Caruana + * + */ +public class M2XML +{ + + /** + * Convert XML date (of the form yyyy-MM-dd) to Date + * + * @param date the xml representation of the date + * @return the date + * @throws ParseException + */ + public static Date deserialiseDate(String date) + throws ParseException + { + Date xmlDate = null; + if (date != null) + { + SimpleDateFormat df = CachingDateFormat.getDateOnlyFormat(); + xmlDate = df.parse(date); + } + return xmlDate; + } + + + /** + * Convert date to XML date (of the form yyyy-MM-dd) + * + * @param date the date + * @return the xml representation of the date + */ + public static String serialiseDate(Date date) + { + String xmlDate = null; + if (date != null) + { + SimpleDateFormat df = CachingDateFormat.getDateOnlyFormat(); + xmlDate = df.format(date); + } + return xmlDate; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/ModelQuery.java b/source/java/org/alfresco/repo/dictionary/ModelQuery.java new file mode 100644 index 0000000000..5c8414d624 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/ModelQuery.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; + + +/** + * Access to model items. + * + * @author David Caruana + * + */ +/*package*/ interface ModelQuery +{ + /** + * Gets the specified data type + * + * @param name name of the data type + * @return data type definition + */ + public DataTypeDefinition getDataType(QName name); + + /** + * Gets the data type for the specified Java Class + * + * @param javaClass the java class + * @return the data type definition (or null, if mapping is not available) + */ + public DataTypeDefinition getDataType(Class javaClass); + + /** + * Gets the specified type + * + * @param name name of the type + * @return type definition + */ + public TypeDefinition getType(QName name); + + /** + * Gets the specified aspect + * + * @param name name of the aspect + * @return aspect definition + */ + public AspectDefinition getAspect(QName name); + + /** + * Gets the specified class + * + * @param name name of the class + * @return class definition + */ + public ClassDefinition getClass(QName name); + + /** + * Gets the specified property + * + * @param name name of the property + * @return property definition + */ + public PropertyDefinition getProperty(QName name); + + /** + * Gets the specified association + * + * @param name name of the association + * @return association definition + */ + public AssociationDefinition getAssociation(QName name); + +} diff --git a/source/java/org/alfresco/repo/dictionary/NamespaceDAO.java b/source/java/org/alfresco/repo/dictionary/NamespaceDAO.java new file mode 100644 index 0000000000..39f81a37f5 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/NamespaceDAO.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import org.alfresco.service.namespace.NamespacePrefixResolver; + + +/** + * Namespace DAO Interface. + * + * This DAO is responsible for retrieving and creating Namespace definitions. + * + * @author David Caruana + */ +public interface NamespaceDAO extends NamespacePrefixResolver +{ + + /** + * Add a namespace URI + * + * @param uri the namespace uri to add + */ + public void addURI(String uri); + + /** + * Remove the specified URI + * + * @param uri the uri to remove + */ + public void removeURI(String uri); + + /** + * Add a namespace prefix + * + * @param prefix the prefix + * @param uri the uri to prefix + */ + public void addPrefix(String prefix, String uri); + + /** + * Remove a namspace prefix + * + * @param prefix the prefix to remove + */ + public void removePrefix(String prefix); + +} diff --git a/source/java/org/alfresco/repo/dictionary/NamespaceDAOImpl.java b/source/java/org/alfresco/repo/dictionary/NamespaceDAOImpl.java new file mode 100644 index 0000000000..cc8ea82c0a --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/NamespaceDAOImpl.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.alfresco.service.namespace.NamespaceException; + +/** + * Simple in-memory namespace DAO + * + * TODO: Remove the many to one mapping of prefixes to URIs + */ +public class NamespaceDAOImpl implements NamespaceDAO +{ + + private List uris = new ArrayList(); + private HashMap prefixes = new HashMap(); + + + public Collection getURIs() + { + return Collections.unmodifiableCollection(uris); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.ref.NamespacePrefixResolver#getPrefixes() + */ + public Collection getPrefixes() + { + return Collections.unmodifiableCollection(prefixes.keySet()); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.NamespaceDAO#addURI(java.lang.String) + */ + public void addURI(String uri) + { + if (uris.contains(uri)) + { + throw new NamespaceException("URI " + uri + " has already been defined"); + } + uris.add(uri); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.NamespaceDAO#addPrefix(java.lang.String, java.lang.String) + */ + public void addPrefix(String prefix, String uri) + { + if (!uris.contains(uri)) + { + throw new NamespaceException("Namespace URI " + uri + " does not exist"); + } + prefixes.put(prefix, uri); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.NamespaceDAO#removeURI(java.lang.String) + */ + public void removeURI(String uri) + { + uris.remove(uri); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.dictionary.impl.NamespaceDAO#removePrefix(java.lang.String) + */ + public void removePrefix(String prefix) + { + prefixes.remove(prefix); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.ref.NamespacePrefixResolver#getNamespaceURI(java.lang.String) + */ + public String getNamespaceURI(String prefix) + { + return prefixes.get(prefix); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.ref.NamespacePrefixResolver#getPrefixes(java.lang.String) + */ + public Collection getPrefixes(String URI) + { + Collection uriPrefixes = new ArrayList(); + for (String key : prefixes.keySet()) + { + String uri = prefixes.get(key); + if ((uri != null) && (uri.equals(URI))) + { + uriPrefixes.add(key); + } + } + return uriPrefixes; + } + +} diff --git a/source/java/org/alfresco/repo/dictionary/TestModel.java b/source/java/org/alfresco/repo/dictionary/TestModel.java new file mode 100644 index 0000000000..d2bcb4be54 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/TestModel.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.dictionary; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Test Model Definitions + */ +public class TestModel +{ + + public static void main(String[] args) + { + if (args != null && args.length > 0 && args[0].equals("-h")) + { + System.out.println("TestModel [model filename]*"); + System.exit(0); + } + + System.out.println("Testing dictionary model definitions..."); + + // construct list of models to test + // include alfresco defaults + List bootstrapModels = new ArrayList(); + bootstrapModels.add("alfresco/model/dictionaryModel.xml"); + bootstrapModels.add("alfresco/model/systemModel.xml"); + bootstrapModels.add("alfresco/model/contentModel.xml"); + bootstrapModels.add("alfresco/model/applicationModel.xml"); + + // include models specified on command line + for (String arg: args) + { + bootstrapModels.add(arg); + } + + for (String model : bootstrapModels) + { + System.out.println(" " + model); + } + + // construct dictionary dao + NamespaceDAO namespaceDAO = new NamespaceDAOImpl(); + DictionaryDAOImpl dictionaryDAO = new DictionaryDAOImpl(namespaceDAO); + + // bootstrap dao + try + { + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + bootstrap.setModels(bootstrapModels); + bootstrap.setDictionaryDAO(dictionaryDAO); + bootstrap.bootstrap(); + System.out.println("Models are valid."); + } + catch(Exception e) + { + System.out.println("Found an invalid model..."); + Throwable t = e; + while (t != null) + { + System.out.println(t.getMessage()); + t = t.getCause(); + } + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.properties b/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.properties new file mode 100644 index 0000000000..bf73f95355 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.properties @@ -0,0 +1,12 @@ +test_dictionarydaotest.description=Model Description + +test_dictionarydaotest.class.test_base.title=Base Title +test_dictionarydaotest.class.test_base.description=Base Description + +test_dictionarydaotest.property.test_prop1.title=Prop1 Title +test_dictionarydaotest.property.test_prop1.description=Prop1 Description + +test_dictionarydaotest.association.test_assoc1.title=Assoc1 Title +test_dictionarydaotest.association.test_assoc1.description=Assoc1 Description + +test_dictionarydaotest.datatype.test_datatype.analyzer=Datatype Analyser diff --git a/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml b/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml new file mode 100644 index 0000000000..d379289328 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml @@ -0,0 +1,148 @@ + + + Alfresco Content Model + Alfresco + 2005-05-30 + 1.0 + + + + + + + + + + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + java.lang.Object + + + + + + + + Base + The Base Type + + + + + d:text + true + + + + + + + + true + false + + + test:base + false + true + + + + + true + true + + + test:referenceable + false + false + + + + + true + true + + + test:referenceable + false + false + + fred + true + + + + + test:referenceable + + + + + test:base + + + + d:text + true + + + + + + + + + test:referenceable + + fred + true + + + + + + an overriden default value + + + + + + test:base + + + + d:text + true + + + + + + + + + + + + Referenceable + The referenceable aspect + + + + + d:int + true + true + + true + false + + + + + + + diff --git a/source/java/org/alfresco/repo/dictionary/m2binding.xml b/source/java/org/alfresco/repo/dictionary/m2binding.xml new file mode 100644 index 0000000000..e9f73379d0 --- /dev/null +++ b/source/java/org/alfresco/repo/dictionary/m2binding.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/domain/ChildAssoc.java b/source/java/org/alfresco/repo/domain/ChildAssoc.java new file mode 100644 index 0000000000..72978ec4ea --- /dev/null +++ b/source/java/org/alfresco/repo/domain/ChildAssoc.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.namespace.QName; + +/** + * Represents a special type of association between nodes, that of the + * parent-child relationship. + * + * @author Derek Hulley + */ +public interface ChildAssoc extends Comparable +{ + /** + * Performs the necessary work on the provided nodes to ensure that a bidirectional + * association is properly set up. + *

    + * The association attributes still have to be set up. + * + * @param parentNode + * @param childNode + * + * @see #setName(String) + * @see #setIsPrimary(boolean) + */ + public void buildAssociation(Node parentNode, Node childNode); + + /** + * Performs the necessary work on the {@link #getParent() parent} and + * {@link #getChild() child} nodes to maintain the inverse association sets + */ + public void removeAssociation(); + + public ChildAssociationRef getChildAssocRef(); + + public Long getId(); + + public Node getParent(); + + public Node getChild(); + + /** + * @return Returns the qualified name of the association type + */ + public QName getTypeQName(); + + /** + * @param assocTypeQName the qualified name of the association type as defined + * in the data dictionary + */ + public void setTypeQName(QName assocTypeQName); + + /** + * @return Returns the qualified name of this association + */ + public QName getQname(); + + /** + * @param qname the qualified name of the association + */ + public void setQname(QName qname); + + public boolean getIsPrimary(); + + public void setIsPrimary(boolean isPrimary); + + /** + * @return Returns the user-assigned index + */ + public int getIndex(); + + /** + * Set the index of this association + * + * @param index the association index + */ + public void setIndex(int index); +} diff --git a/source/java/org/alfresco/repo/domain/Node.java b/source/java/org/alfresco/repo/domain/Node.java new file mode 100644 index 0000000000..0779fe05de --- /dev/null +++ b/source/java/org/alfresco/repo/domain/Node.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Interface for persistent node objects. + *

    + * Specific instances of nodes are unique, but may share GUIDs across stores. + * + * @author Derek Hulley + */ +public interface Node +{ + /** + * @return Returns the unique key for this node + */ + public NodeKey getKey(); + + /** + * @param key the unique node key + */ + public void setKey(NodeKey key); + + public Store getStore(); + + public void setStore(Store store); + + public QName getTypeQName(); + + public void setTypeQName(QName typeQName); + + /** + * Set the status of the node. This is compulsory, but a node + * status may exist without a node being present. + * + * @param nodeStatus + */ + public void setStatus(NodeStatus nodeStatus); + + /** + * Get the mandatory node status object + * + * @return + */ + public NodeStatus getStatus(); + + public Set getAspects(); + + /** + * @return Returns all the regular associations for which this node is a target + */ + public Collection getSourceNodeAssocs(); + + /** + * @return Returns all the regular associations for which this node is a source + */ + public Collection getTargetNodeAssocs(); + + public Collection getChildAssocs(); + + public Collection getParentAssocs(); + + public Map getProperties(); + + /** + * Convenience method to get the reference to the node + * + * @return Returns the reference to this node + */ + public NodeRef getNodeRef(); +} diff --git a/source/java/org/alfresco/repo/domain/NodeAssoc.java b/source/java/org/alfresco/repo/domain/NodeAssoc.java new file mode 100644 index 0000000000..d4ccaca1f5 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/NodeAssoc.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.namespace.QName; + +/** + * Represents a generic association between two nodes. The association is named + * and bidirectional by default. + * + * @author Derek Hulley + */ +public interface NodeAssoc +{ + public long getId(); + + /** + * Wires up the necessary bits on the source and target nodes so that the association + * is immediately bidirectional. + *

    + * The association attributes still have to be set. + * + * @param sourceNode + * @param targetNode + * + * @see #setName(String) + */ + public void buildAssociation(Node sourceNode, Node targetNode); + + /** + * Performs the necessary work on the {@link #getSource()() source} and + * {@link #getTarget()() target} nodes to maintain the inverse association sets + */ + public void removeAssociation(); + + public AssociationRef getNodeAssocRef(); + + public Node getSource(); + + public Node getTarget(); + + /** + * @return Returns the qualified name of this association type + */ + public QName getTypeQName(); + + /** + * @param qname the qualified name of the association type + */ + public void setTypeQName(QName qname); +} diff --git a/source/java/org/alfresco/repo/domain/NodeKey.java b/source/java/org/alfresco/repo/domain/NodeKey.java new file mode 100644 index 0000000000..7d45956d72 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/NodeKey.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import java.io.Serializable; + +import org.alfresco.util.EqualsHelper; + +/** + * Compound key for persistence of {@link org.alfresco.repo.domain.Node} + * + * @author Derek Hulley + */ +public class NodeKey implements Serializable +{ + private static final long serialVersionUID = 3258695403221300023L; + + private String guid; + private String protocol; + private String identifier; + + public NodeKey() + { + } + + public NodeKey(StoreKey storeKey, String guid) + { + setGuid(guid); + setProtocol(storeKey.getProtocol()); + setIdentifier(storeKey.getIdentifier()); + } + + public NodeKey(String protocol, String identifier, String guid) + { + setGuid(guid); + setProtocol(protocol); + setIdentifier(identifier); + } + + public String toString() + { + return ("NodeKey[" + + " id=" + guid + + ", protocol=" + protocol + + ", identifier=" + identifier + + "]"); + } + + public int hashCode() + { + return this.guid.hashCode(); + } + + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + else if (!(obj instanceof NodeKey)) + { + return false; + } + NodeKey that = (NodeKey) obj; + return (EqualsHelper.nullSafeEquals(this.guid, that.guid) && + EqualsHelper.nullSafeEquals(this.protocol, that.protocol) && + EqualsHelper.nullSafeEquals(this.identifier, that.identifier) + ); + } + + public String getGuid() + { + return guid; + } + + /** + * Tamper-proof method only to be used by introspectors + */ + private void setGuid(String id) + { + this.guid = id; + } + + public String getProtocol() + { + return protocol; + } + + /** + * Tamper-proof method only to be used by introspectors + */ + private void setProtocol(String protocol) + { + this.protocol = protocol; + } + + public String getIdentifier() + { + return identifier; + } + + /** + * Tamper-proof method only to be used by introspectors + */ + private void setIdentifier(String identifier) + { + this.identifier = identifier; + } +} diff --git a/source/java/org/alfresco/repo/domain/NodeStatus.java b/source/java/org/alfresco/repo/domain/NodeStatus.java new file mode 100644 index 0000000000..b6b2046958 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/NodeStatus.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +/** + * Interface for persistent node status objects. + *

    + * The node status records the liveness and change times of a node. It follows + * that a node might not exist (have been deleted) when the + * node status still exists. + * + * @author Derek Hulley + */ +public interface NodeStatus +{ + /** + * @return Returns the unique key for this node status + */ + public NodeKey getKey(); + + /** + * @param key the unique key + */ + public void setKey(NodeKey key); + + public String getChangeTxnId(); + + public void setChangeTxnId(String txnId); + + public void setDeleted(boolean deleted); + + public boolean isDeleted(); +} diff --git a/source/java/org/alfresco/repo/domain/PropertyValue.java b/source/java/org/alfresco/repo/domain/PropertyValue.java new file mode 100644 index 0000000000..0b28cea323 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/PropertyValue.java @@ -0,0 +1,606 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Immutable property value storage class. + *

    + * The + * + * @author Derek Hulley + */ +public class PropertyValue implements Cloneable, Serializable +{ + private static final long serialVersionUID = -497902497351493075L; + + private static Log logger = LogFactory.getLog(PropertyValue.class); + + /** potential value types */ + private static enum ValueType + { + NULL + { + @Override + Serializable convert(Serializable value) + { + return null; + } + }, + BOOLEAN + { + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Boolean.class, value); + } + }, + INTEGER + { + @Override + protected ValueType getPersistedType() + { + return ValueType.LONG; + } + + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Integer.class, value); + } + }, + LONG + { + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Long.class, value); + } + }, + FLOAT + { + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Float.class, value); + } + }, + DOUBLE + { + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Double.class, value); + } + }, + STRING + { + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(String.class, value); + } + }, + DATE + { + @Override + protected ValueType getPersistedType() + { + return ValueType.STRING; + } + + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Date.class, value); + } + }, + SERIALIZABLE + { + @Override + Serializable convert(Serializable value) + { + return value; + } + }, + CONTENT + { + @Override + protected ValueType getPersistedType() + { + return ValueType.STRING; + } + + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(ContentData.class, value); + } + }, + NODEREF + { + @Override + protected ValueType getPersistedType() + { + return ValueType.STRING; + } + + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(NodeRef.class, value); + } + }, + QNAME + { + @Override + protected ValueType getPersistedType() + { + return ValueType.STRING; + } + + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(QName.class, value); + } + }, + PATH + { + @Override + protected ValueType getPersistedType() + { + return ValueType.SERIALIZABLE; + } + + @Override + Serializable convert(Serializable value) + { + return DefaultTypeConverter.INSTANCE.convert(Path.class, value); + } + }; + + /** override if the type gets persisted in a different format */ + protected ValueType getPersistedType() + { + return this; + } + + /** + * @see DefaultTypeConverter.INSTANCE#convert(Class, Object) + */ + abstract Serializable convert(Serializable value); + + protected ArrayList convert(Collection collection) + { + ArrayList arrayList = new ArrayList(collection.size()); + for (Object object : collection) + { + Serializable newValue = null; + if (object != null) + { + if (!(object instanceof Serializable)) + { + throw new AlfrescoRuntimeException("Collection values must contain Serializable instances: \n" + + " value type: " + this + "\n" + + " collection: " + collection + "\n" + + " value: " + object); + } + Serializable value = (Serializable) object; + newValue = convert(value); + } + arrayList.add(newValue); + } + // done + return arrayList; + } + } + + /** a mapping from a property type QName to the corresponding value type */ + private static Map valueTypesByPropertyType; + static + { + valueTypesByPropertyType = new HashMap(17); + valueTypesByPropertyType.put(DataTypeDefinition.ANY, ValueType.SERIALIZABLE); + valueTypesByPropertyType.put(DataTypeDefinition.BOOLEAN, ValueType.BOOLEAN); + valueTypesByPropertyType.put(DataTypeDefinition.INT, ValueType.INTEGER); + valueTypesByPropertyType.put(DataTypeDefinition.LONG, ValueType.LONG); + valueTypesByPropertyType.put(DataTypeDefinition.DOUBLE, ValueType.DOUBLE); + valueTypesByPropertyType.put(DataTypeDefinition.FLOAT, ValueType.FLOAT); + valueTypesByPropertyType.put(DataTypeDefinition.DATE, ValueType.DATE); + valueTypesByPropertyType.put(DataTypeDefinition.DATETIME, ValueType.DATE); + valueTypesByPropertyType.put(DataTypeDefinition.CATEGORY, ValueType.NODEREF); + valueTypesByPropertyType.put(DataTypeDefinition.CONTENT, ValueType.CONTENT); + valueTypesByPropertyType.put(DataTypeDefinition.TEXT, ValueType.STRING); + valueTypesByPropertyType.put(DataTypeDefinition.NODE_REF, ValueType.NODEREF); + valueTypesByPropertyType.put(DataTypeDefinition.PATH, ValueType.PATH); + valueTypesByPropertyType.put(DataTypeDefinition.QNAME, ValueType.QNAME); + } + + /** the type of the property, prior to serialization persistence */ + private ValueType actualType; + /** true if the property values are contained in a collection */ + private boolean isMultiValued; + /** the type of persistence used */ + private ValueType persistedType; + + private Boolean booleanValue; + private Long longValue; + private Float floatValue; + private Double doubleValue; + private String stringValue; + private Serializable serializableValue; + + /** + * default constructor + */ + public PropertyValue() + { + } + + /** + * Construct a new property value. + * + * @param typeQName the dictionary-defined property type to store the property as + * @param value the value to store. This will be converted into a format compatible + * with the type given + * + * @throws java.lang.UnsupportedOperationException if the value cannot be converted to the + * type given + */ + public PropertyValue(QName typeQName, Serializable value) + { + this.actualType = makeValueType(typeQName); + if (value == null) + { + setPersistedValue(ValueType.NULL, null); + setMultiValued(false); + } + else if (value instanceof Collection) + { + Collection collection = (Collection) value; + ValueType collectionValueType = makeValueType(typeQName); + // convert the collection values - we need to do this to ensure that the + // values provided conform to the given type + ArrayList convertedCollection = collectionValueType.convert(collection); + // the persisted type is, nonetheless, a serializable + setPersistedValue(ValueType.SERIALIZABLE, convertedCollection); + setMultiValued(true); + } + else + { + // get the persisted type + ValueType valueType = makeValueType(typeQName); + ValueType persistedValueType = valueType.getPersistedType(); + // convert to the persistent type + value = persistedValueType.convert(value); + setPersistedValue(persistedValueType, value); + setMultiValued(false); + } + } + + /** + * Helper method to convert the type QName into a ValueType + * + * @return Returns the ValueType - never null + */ + private ValueType makeValueType(QName typeQName) + { + ValueType valueType = valueTypesByPropertyType.get(typeQName); + if (valueType == null) + { + throw new AlfrescoRuntimeException( + "Property type not recognised: \n" + + " type: " + typeQName + "\n" + + " property: " + this); + } + return valueType; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (obj instanceof PropertyValue) + { + PropertyValue that = (PropertyValue) obj; + return (this.actualType.equals(that.actualType) && + EqualsHelper.nullSafeEquals(this.booleanValue, that.booleanValue) && + EqualsHelper.nullSafeEquals(this.longValue, that.longValue) && + EqualsHelper.nullSafeEquals(this.floatValue, that.floatValue) && + EqualsHelper.nullSafeEquals(this.doubleValue, that.doubleValue) && + EqualsHelper.nullSafeEquals(this.stringValue, that.stringValue) && + EqualsHelper.nullSafeEquals(this.serializableValue, that.serializableValue) + ); + + } + else + { + return false; + } + } + + @Override + public int hashCode() + { + int h = 0; + if (actualType != null) + h = actualType.hashCode(); + Serializable persistedValue = getPersistedValue(); + if (persistedValue != null) + h += 17 * persistedValue.hashCode(); + return h; + } + + @Override + public Object clone() throws CloneNotSupportedException + { + return super.clone(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(128); + sb.append("PropertyValue") + .append("[actual-type=").append(actualType) + .append(", multi-valued=").append(isMultiValued) + .append(", value-type=").append(persistedType) + .append(", value=").append(getPersistedValue()) + .append("]"); + return sb.toString(); + } + + public String getActualType() + { + return actualType.toString(); + } + + public void setActualType(String actualType) + { + this.actualType = ValueType.valueOf(actualType); + } + + public boolean isMultiValued() + { + return isMultiValued; + } + + public void setMultiValued(boolean isMultiValued) + { + this.isMultiValued = isMultiValued; + } + + public String getPersistedType() + { + return persistedType.toString(); + } + public void setPersistedType(String persistedType) + { + this.persistedType = ValueType.valueOf(persistedType); + } + + /** + * Stores the value in the correct slot based on the type of persistence requested. + * No conversion is done. + * + * @param persistedType the value type + * @param value the value - it may only be null if the persisted type is {@link ValueType#NULL} + */ + public void setPersistedValue(ValueType persistedType, Serializable value) + { + switch (persistedType) + { + case NULL: + if (value != null) + { + throw new AlfrescoRuntimeException("Value must be null for persisted type: " + persistedType); + } + break; + case BOOLEAN: + this.booleanValue = (Boolean) value; + break; + case LONG: + this.longValue = (Long) value; + break; + case FLOAT: + this.floatValue = (Float) value; + break; + case DOUBLE: + this.doubleValue = (Double) value; + break; + case STRING: + this.stringValue = (String) value; + break; + case SERIALIZABLE: + this.serializableValue = (Serializable) value; + break; + default: + throw new AlfrescoRuntimeException("Unrecognised value type: " + persistedType); + } + // we store the type that we persisted as + this.persistedType = persistedType; + } + + /** + * @return Returns the persisted value, keying off the persisted value type + */ + private Serializable getPersistedValue() + { + switch (persistedType) + { + case NULL: + return null; + case BOOLEAN: + return this.booleanValue; + case LONG: + return this.longValue; + case FLOAT: + return this.floatValue; + case DOUBLE: + return this.doubleValue; + case STRING: + return this.stringValue; + case SERIALIZABLE: + return this.serializableValue; + default: + throw new AlfrescoRuntimeException("Unrecognised value type: " + persistedType); + } + } + + /** + * Fetches the value as a desired type. Collections (i.e. multi-valued properties) + * will be converted as a whole to ensure that all the values returned within the + * collection match the given type. + * + * @param typeQName the type required for the return value + * @return Returns the value of this property as the desired type, or a Collection + * of values of the required type + * + * @throws java.lang.UnsupportedOperationException if the value cannot be converted to the + * type given + * + * @see DataTypeDefinition#ANY The static qualified names for the types + */ + public Serializable getValue(QName typeQName) + { + // first check for null + + ValueType requiredType = makeValueType(typeQName); + + // we need to convert + Serializable ret = null; + if (persistedType == ValueType.NULL) + { + ret = null; + } + else if (this.isMultiValued) + { + // collections are always stored + Collection collection = (Collection) this.serializableValue; + // convert the collection values - we need to do this to ensure that the + // values provided conform to the given type + ArrayList convertedCollection = requiredType.convert(collection); + ret = convertedCollection; + } + else + { + Serializable persistedValue = getPersistedValue(); + // convert the type + ret = requiredType.convert(persistedValue); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Fetched value: \n" + + " property value: " + this + "\n" + + " requested type: " + requiredType + "\n" + + " result: " + ret); + } + return ret; + } + + public boolean getBooleanValue() + { + if (booleanValue == null) + return false; + else + return booleanValue.booleanValue(); + } + public void setBooleanValue(boolean value) + { + this.booleanValue = Boolean.valueOf(value); + } + + public long getLongValue() + { + if (longValue == null) + return 0; + else + return longValue.longValue(); + } + public void setLongValue(long value) + { + this.longValue = Long.valueOf(value); + } + + public float getFloatValue() + { + if (floatValue == null) + return 0.0F; + else + return floatValue.floatValue(); + } + public void setFloatValue(float value) + { + this.floatValue = Float.valueOf(value); + } + + public double getDoubleValue() + { + if (doubleValue == null) + return 0.0; + else + return doubleValue.doubleValue(); + } + public void setDoubleValue(double value) + { + this.doubleValue = Double.valueOf(value); + } + + public String getStringValue() + { + return stringValue; + } + public void setStringValue(String value) + { + this.stringValue = value; + } + + public Serializable getSerializableValue() + { + return serializableValue; + } + public void setSerializableValue(Serializable value) + { + this.serializableValue = value; + } +} diff --git a/source/java/org/alfresco/repo/domain/Store.java b/source/java/org/alfresco/repo/domain/Store.java new file mode 100644 index 0000000000..b54d16bca5 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/Store.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import org.alfresco.repo.domain.StoreKey; +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * Represents a store entity + * + * @author Derek Hulley + */ +public interface Store +{ + /** + * @return Returns the key for the class + */ + public StoreKey getKey(); + + /** + * @param key the key uniquely identifying this store + */ + public void setKey(StoreKey key); + + /** + * @return Returns the root of the store + */ + public Node getRootNode(); + + /** + * @param rootNode mandatory association to the root of the store + */ + public void setRootNode(Node rootNode); + + /** + * Convenience method to access the reference + * @return Returns the reference to the store + */ + public StoreRef getStoreRef(); +} diff --git a/source/java/org/alfresco/repo/domain/StoreKey.java b/source/java/org/alfresco/repo/domain/StoreKey.java new file mode 100644 index 0000000000..b545cdabfa --- /dev/null +++ b/source/java/org/alfresco/repo/domain/StoreKey.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +import java.io.Serializable; + +import org.alfresco.util.EqualsHelper; + +/** + * Compound key for persistence of {@link org.alfresco.repo.domain.Store} + * + * @author Derek Hulley + */ +public class StoreKey implements Serializable +{ + private static final long serialVersionUID = 3618140052220096569L; + + private String protocol; + private String identifier; + + public StoreKey() + { + } + + public StoreKey(String protocol, String identifier) + { + setProtocol(protocol); + setIdentifier(identifier); + } + + public String toString() + { + return ("StoreKey[" + + " protocol=" + protocol + + ", identifier=" + identifier + + "]"); + } + + public int hashCode() + { + return (this.protocol.hashCode() + this.identifier.hashCode()); + } + + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + else if (!(obj instanceof StoreKey)) + { + return false; + } + StoreKey that = (StoreKey) obj; + return (EqualsHelper.nullSafeEquals(this.protocol, that.protocol) && + EqualsHelper.nullSafeEquals(this.identifier, that.identifier)); + } + + public String getProtocol() + { + return protocol; + } + + /** + * Tamper-proof method only to be used by introspectors + */ + private void setProtocol(String protocol) + { + this.protocol = protocol; + } + + public String getIdentifier() + { + return identifier; + } + + /** + * Tamper-proof method only to be used by introspectors + */ + private void setIdentifier(String identifier) + { + this.identifier = identifier; + } +} diff --git a/source/java/org/alfresco/repo/domain/VersionCount.java b/source/java/org/alfresco/repo/domain/VersionCount.java new file mode 100644 index 0000000000..759b7d1832 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/VersionCount.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain; + +/** + * Represents a version count entity for a particular store. + * + * @author Derek Hulley + */ +public interface VersionCount +{ + /** + * @return Returns the key for the version counter + */ + public StoreKey getKey(); + + /** + * @param key the key uniquely identifying this version counter + */ + public void setKey(StoreKey key); + + /** + * Increments and returns the next version counter associated with this + * store. + * + * @return Returns the next version counter in the sequence + * + * @see #getVersionCount() + */ + public int incrementVersionCount(); + + /** + * Reset the store's version counter + */ + public void resetVersionCount(); + + /** + * Retrieve the current version counter + * + * @return Returns a current version counter + * + * @see #incrementVersionCount() + */ + public int getVersionCount(); +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/ChildAssocImpl.java b/source/java/org/alfresco/repo/domain/hibernate/ChildAssocImpl.java new file mode 100644 index 0000000000..25c86778af --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/ChildAssocImpl.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; + +/** + * @author Derek Hulley + */ +public class ChildAssocImpl implements ChildAssoc +{ + private Long id; + private Node parent; + private Node child; + private QName typeQName; + private QName qName; + private boolean isPrimary; + private int index; + private transient ChildAssociationRef childAssocRef; + + public ChildAssocImpl() + { + setIndex(Integer.MAX_VALUE); // comes last + } + + public void buildAssociation(Node parentNode, Node childNode) + { + // add the forward associations + this.setParent(parentNode); + this.setChild(childNode); + // add the inverse associations + parentNode.getChildAssocs().add(this); + childNode.getParentAssocs().add(this); + } + + public void removeAssociation() + { + // maintain inverse assoc from parent node to this instance + this.getParent().getChildAssocs().remove(this); + // maintain inverse assoc from child node to this instance + this.getChild().getParentAssocs().remove(this); + } + + public synchronized ChildAssociationRef getChildAssocRef() + { + if (childAssocRef == null) + { + childAssocRef = new ChildAssociationRef( + this.typeQName, + getParent().getNodeRef(), + this.qName, + getChild().getNodeRef(), + this.isPrimary, + -1); + } + return childAssocRef; + } + + public String toString() + { + StringBuffer sb = new StringBuffer(32); + sb.append("ChildAssoc") + .append("[ parent=").append(parent) + .append(", child=").append(child) + .append(", name=").append(getQname()) + .append(", isPrimary=").append(isPrimary) + .append("]"); + return sb.toString(); + } + + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (obj == this) + { + return true; + } + else if (!(obj instanceof ChildAssoc)) + { + return false; + } + ChildAssoc that = (ChildAssoc) obj; + return (this.getIsPrimary() == that.getIsPrimary() + && EqualsHelper.nullSafeEquals(this.getTypeQName(), that.getTypeQName()) + && EqualsHelper.nullSafeEquals(this.getQname(), that.getQname()) + && EqualsHelper.nullSafeEquals(this.getParent(), that.getParent()) + && EqualsHelper.nullSafeEquals(this.getChild(), that.getChild())); + } + + public int hashCode() + { + return (qName == null ? 0 : qName.hashCode()); + } + + /** + * Orders the child associations by ID. A smaller ID has a higher priority. + * This may change once we introduce a changeable index against which to order. + */ + public int compareTo(ChildAssoc another) + { + if (this == another) + { + return 0; + } + + int thisIndex = this.getIndex(); + int anotherIndex = another.getIndex(); + + Long thisId = this.getId(); + Long anotherId = another.getId(); + + if (thisId == null) // this ID has not been set, make this instance greater + { + return -1; + } + else if (anotherId == null) // other ID has not been set, make this instance lesser + { + return 1; + } + else if (thisIndex == anotherIndex) // use the explicit index + { + return thisId.compareTo(anotherId); + } + else // fallback on order of creation + { + return (thisIndex > anotherIndex) ? 1 : -1; // a lower index, make this instance lesser + } + } + + public Long getId() + { + return id; + } + + /** + * For Hibernate use + */ + private void setId(Long id) + { + this.id = id; + } + + public Node getParent() + { + return parent; + } + + /** + * For Hibernate use + */ + private void setParent(Node parentNode) + { + this.parent = parentNode; + } + + public Node getChild() + { + return child; + } + + /** + * For Hibernate use + */ + private void setChild(Node node) + { + child = node; + } + + public QName getTypeQName() + { + return typeQName; + } + + public void setTypeQName(QName typeQName) + { + this.typeQName = typeQName; + } + + public QName getQname() + { + return qName; + } + + public void setQname(QName qname) + { + this.qName = qname; + } + + public boolean getIsPrimary() + { + return isPrimary; + } + + public void setIsPrimary(boolean isPrimary) + { + this.isPrimary = isPrimary; + } + + public int getIndex() + { + return index; + } + + public void setIndex(int index) + { + this.index = index; + } +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java b/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java new file mode 100644 index 0000000000..dd8b5b4838 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.NodeAssoc; +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.repo.domain.PropertyValue; +import org.alfresco.repo.domain.Store; +import org.alfresco.repo.domain.StoreKey; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; + +/** + * Test persistence and retrieval of Hibernate-specific implementations of the + * {@link org.alfresco.repo.domain.Node} interface + * + * @author Derek Hulley + */ +public class HibernateNodeTest extends BaseSpringTest +{ + private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/HibernateNodeTest"; + + private Store store; + + public HibernateNodeTest() + { + } + + protected void onSetUpInTransaction() throws Exception + { + store = new StoreImpl(); + StoreKey storeKey = new StoreKey(StoreRef.PROTOCOL_WORKSPACE, + "TestWorkspace@" + System.currentTimeMillis()); + store.setKey(storeKey); + // persist so that it is present in the hibernate cache + getSession().save(store); + } + + protected void onTearDownInTransaction() + { + // force a flush to ensure that the database updates succeed + getSession().flush(); + getSession().clear(); + } + + public void testSetUp() throws Exception + { + assertNotNull("Workspace not initialised", store); + } + + public void testGetStore() throws Exception + { + NodeKey key = new NodeKey("Random Protocol", "Random Identifier", "AAA"); + // create the node status + NodeStatus nodeStatus = new NodeStatusImpl(); + nodeStatus.setKey(key); + nodeStatus.setDeleted(false); + nodeStatus.setChangeTxnId("txn:123"); + getSession().save(nodeStatus); + // create a new Node + Node node = new NodeImpl(); + node.setKey(key); + node.setStore(store); // not meaningful as it contradicts the key + node.setTypeQName(ContentModel.TYPE_CONTAINER); + node.setStatus(nodeStatus); + // persist it + try + { + Serializable id = getSession().save(node); + fail("No store exists"); + } + catch (Throwable e) + { + // expected + } + // this should not solve the problem + node.setStore(store); + // persist it + try + { + Serializable id = getSession().save(node); + fail("Setting store does not persist protocol and identifier attributes"); + } + catch (Throwable e) + { + // expected + } + + // fix the key + key = new NodeKey(store.getKey().getProtocol(), store.getKey().getIdentifier(), "AAA"); + node.setKey(key); + // now it should work + Serializable id = getSession().save(node); + + // throw the reference away and get the a new one for the id + node = (Node) getSession().load(NodeImpl.class, id); + assertNotNull("Node not found", node); + // check that the store has been loaded + Store loadedStore = node.getStore(); + assertNotNull("Store not present on node", loadedStore); + assertEquals("Incorrect store key", store, loadedStore); + } + + public void testNodeStatus() + { + NodeKey key = new NodeKey(store.getKey(), "AAA"); + // create the node status + NodeStatus nodeStatus = new NodeStatusImpl(); + nodeStatus.setKey(key); + nodeStatus.setDeleted(false); + nodeStatus.setChangeTxnId("txn:123"); + getSession().save(nodeStatus); + + // it must be able to exist without the node + flushAndClear(); + + // create a new Node + Node node = new NodeImpl(); + node.setStore(store); + node.setKey(key); + node.setStore(store); // not meaningful as it contradicts the key + node.setTypeQName(ContentModel.TYPE_CONTAINER); + node.setStatus(nodeStatus); + Serializable id = getSession().save(node); + + // flush + flushAndClear(); + + // is the status retrievable + node = (Node) getSession().get(NodeImpl.class, id); + nodeStatus = node.getStatus(); + // change the values + nodeStatus.setChangeTxnId("txn:456"); + nodeStatus.setDeleted(true); + // delete the node + getSession().delete(node); + + // flush + flushAndClear(); + } + + /** + * Check that properties can be persisted and retrieved + */ + public void testProperties() throws Exception + { + NodeKey key = new NodeKey(store.getKey(), "AAA"); + // create the node status + NodeStatus nodeStatus = new NodeStatusImpl(); + nodeStatus.setKey(key); + nodeStatus.setDeleted(false); + nodeStatus.setChangeTxnId("txn:123"); + getSession().save(nodeStatus); + // create a new Node + Node node = new NodeImpl(); + node.setKey(key); + node.setTypeQName(ContentModel.TYPE_CONTAINER); + node.setStatus(nodeStatus); + // give it a property map + Map propertyMap = new HashMap(5); + QName propertyQName = QName.createQName("{}A"); + PropertyValue propertyValue = new PropertyValue(DataTypeDefinition.TEXT, "AAA"); + propertyMap.put(propertyQName, propertyValue); + node.getProperties().putAll(propertyMap); + // persist it + Serializable id = getSession().save(node); + + // throw the reference away and get the a new one for the id + node = (Node) getSession().load(NodeImpl.class, id); + assertNotNull("Node not found", node); + // extract the Map + propertyMap = node.getProperties(); + assertNotNull("Map not persisted", propertyMap); + // ensure that the value is present + assertNotNull("Property value not present in map", QName.createQName("{}A")); + } + + /** + * Check that aspect qnames can be added and removed from a node and that they + * are persisted correctly + */ + public void testAspects() throws Exception + { + NodeKey key = new NodeKey(store.getKey(), GUID.generate()); + // create the node status + NodeStatus nodeStatus = new NodeStatusImpl(); + nodeStatus.setKey(key); + nodeStatus.setDeleted(false); + nodeStatus.setChangeTxnId("txn:123"); + getSession().save(nodeStatus); + // make a real node + Node node = new NodeImpl(); + node.setKey(key); + node.setStore(store); + node.setTypeQName(ContentModel.TYPE_CMOBJECT); + node.setStatus(nodeStatus); + + // add some aspects + QName aspect1 = QName.createQName(TEST_NAMESPACE, "1"); + QName aspect2 = QName.createQName(TEST_NAMESPACE, "2"); + QName aspect3 = QName.createQName(TEST_NAMESPACE, "3"); + QName aspect4 = QName.createQName(TEST_NAMESPACE, "4"); + Set aspects = node.getAspects(); + aspects.add(aspect1); + aspects.add(aspect2); + aspects.add(aspect3); + aspects.add(aspect4); + assertFalse("Set did not eliminate duplicate aspect qname", aspects.add(aspect4)); + + // persist + Serializable id = getSession().save(node); + + // flush and clear + flushAndClear(); + + // get node and check aspects + node = (Node) getSession().get(NodeImpl.class, id); + assertNotNull("Node not persisted", node); + aspects = node.getAspects(); + assertEquals("Not all aspects persisted", 4, aspects.size()); + } + + public void testNodeAssoc() throws Exception + { + NodeKey sourceKey = new NodeKey(store.getKey(), GUID.generate()); + // make a source node + NodeStatus sourceNodeStatus = new NodeStatusImpl(); + sourceNodeStatus.setKey(sourceKey); + sourceNodeStatus.setDeleted(false); + sourceNodeStatus.setChangeTxnId("txn:123"); + getSession().save(sourceNodeStatus); + Node sourceNode = new NodeImpl(); + sourceNode.setKey(sourceKey); + sourceNode.setStore(store); + sourceNode.setTypeQName(ContentModel.TYPE_CMOBJECT); + sourceNode.setStatus(sourceNodeStatus); + Serializable realNodeKey = getSession().save(sourceNode); + + // make a container node + NodeKey targetKey = new NodeKey(store.getKey(), GUID.generate()); + NodeStatus targetNodeStatus = new NodeStatusImpl(); + targetNodeStatus.setKey(targetKey); + targetNodeStatus.setDeleted(false); + targetNodeStatus.setChangeTxnId("txn:123"); + getSession().save(targetNodeStatus); + Node targetNode = new NodeImpl(); + targetNode.setKey(targetKey); + targetNode.setStore(store); + targetNode.setTypeQName(ContentModel.TYPE_CONTAINER); + targetNode.setStatus(targetNodeStatus); + Serializable containerNodeKey = getSession().save(targetNode); + + // create an association between them + NodeAssoc assoc = new NodeAssocImpl(); + assoc.setTypeQName(QName.createQName("next")); + assoc.buildAssociation(sourceNode, targetNode); + getSession().save(assoc); + + // make another association between the same two nodes + assoc = new NodeAssocImpl(); + assoc.setTypeQName(QName.createQName("helper")); + assoc.buildAssociation(sourceNode, targetNode); + getSession().save(assoc); + + // flush and clear the session + getSession().flush(); + getSession().clear(); + + // reload the source + sourceNode = (Node) getSession().get(NodeImpl.class, sourceKey); + assertNotNull("Source node not found", sourceNode); + // check that the associations are present + assertEquals("Expected exactly 2 target assocs", 2, sourceNode.getTargetNodeAssocs().size()); + + // reload the target + targetNode = (Node) getSession().get(NodeImpl.class, targetKey); + assertNotNull("Target node not found", targetNode); + // check that the associations are present + assertEquals("Expected exactly 2 source assocs", 2, targetNode.getSourceNodeAssocs().size()); + } + + public void testChildAssoc() throws Exception + { + // make a content node + NodeKey key = new NodeKey(store.getKey(), GUID.generate()); + NodeStatus contentNodeStatus = new NodeStatusImpl(); + contentNodeStatus.setKey(key); + contentNodeStatus.setDeleted(false); + contentNodeStatus.setChangeTxnId("txn:123"); + getSession().save(contentNodeStatus); + Node contentNode = new NodeImpl(); + contentNode.setKey(key); + contentNode.setStore(store); + contentNode.setTypeQName(ContentModel.TYPE_CONTENT); + contentNode.setStatus(contentNodeStatus); + Serializable contentNodeKey = getSession().save(contentNode); + + // make a container node + key = new NodeKey(store.getKey(), GUID.generate()); + NodeStatus containerNodeStatus = new NodeStatusImpl(); + containerNodeStatus.setKey(key); + containerNodeStatus.setDeleted(false); + containerNodeStatus.setChangeTxnId("txn:123"); + getSession().save(containerNodeStatus); + Node containerNode = new NodeImpl(); + containerNode.setKey(key); + containerNode.setStore(store); + containerNode.setTypeQName(ContentModel.TYPE_CONTAINER); + containerNode.setStatus(containerNodeStatus); + Serializable containerNodeKey = getSession().save(containerNode); + // create an association to the content + ChildAssoc assoc1 = new ChildAssocImpl(); + assoc1.setIsPrimary(true); + assoc1.setTypeQName(QName.createQName(null, "type1")); + assoc1.setQname(QName.createQName(null, "number1")); + assoc1.buildAssociation(containerNode, contentNode); + getSession().save(assoc1); + + // make another association between the same two parent and child nodes + ChildAssoc assoc2 = new ChildAssocImpl(); + assoc2.setIsPrimary(true); + assoc2.setTypeQName(QName.createQName(null, "type1")); + assoc2.setQname(QName.createQName(null, "number2")); + assoc2.buildAssociation(containerNode, contentNode); + getSession().save(assoc2); + + assertFalse("Hashcode incorrent", assoc2.hashCode() == 0); + assertNotSame("Assoc equals failure", assoc1, assoc2); + +// flushAndClear(); + + // reload the container + containerNode = (Node) getSession().get(NodeImpl.class, containerNodeKey); + assertNotNull("Node not found", containerNode); + // check + assertEquals("Expected exactly 2 children", 2, containerNode.getChildAssocs().size()); + for (Iterator iterator = containerNode.getChildAssocs().iterator(); iterator.hasNext(); /**/) + { + ChildAssoc assoc = (ChildAssoc) iterator.next(); + // the node id must be known + assertNotNull("Node not populated on assoc", assoc.getChild()); + assertEquals("Node key on child assoc is incorrect", contentNodeKey, + assoc.getChild().getKey()); + } + + // check that we can traverse the association from the child + Collection parentAssocs = contentNode.getParentAssocs(); + assertEquals("Expected exactly 2 parent assocs", 2, parentAssocs.size()); + parentAssocs = new HashSet(parentAssocs); + for (ChildAssoc assoc : parentAssocs) + { + // maintain inverse assoc sets + assoc.removeAssociation(); + // remove the assoc + getSession().delete(assoc); + } + + // check that the child now has zero parents + parentAssocs = contentNode.getParentAssocs(); + assertEquals("Expected exactly 0 parent assocs", 0, parentAssocs.size()); + } + + /** + * Allows tracing of L2 cache + */ + public void testCaching() throws Exception + { + NodeKey key = new NodeKey(store.getKey(), GUID.generate()); + + // make a node + NodeStatus nodeStatus = new NodeStatusImpl(); + nodeStatus.setKey(key); + nodeStatus.setDeleted(false); + nodeStatus.setChangeTxnId("txn:123"); + getSession().save(nodeStatus); + Node node = new NodeImpl(); + node.setKey(key); + node.setStore(store); + node.setTypeQName(ContentModel.TYPE_CONTENT); + node.setStatus(nodeStatus); + getSession().save(node); + + // add some aspects to the node + Set aspects = node.getAspects(); + aspects.add(ContentModel.ASPECT_AUDITABLE); + + // add some properties + Map properties = node.getProperties(); + properties.put(ContentModel.PROP_NAME, new PropertyValue(DataTypeDefinition.TEXT, "ABC")); + + // check that the session hands back the same instance + Node checkNode = (Node) getSession().get(NodeImpl.class, key); + assertNotNull(checkNode); + assertTrue("Node retrieved was not same instance", checkNode == node); + + Set checkAspects = checkNode.getAspects(); + assertTrue("Aspect set retrieved was not the same instance", checkAspects == aspects); + assertEquals("Incorrect number of aspects", 1, checkAspects.size()); + QName checkQName = (QName) checkAspects.toArray()[0]; + assertTrue("QName retrieved was not the same instance", checkQName == ContentModel.ASPECT_AUDITABLE); + + Map checkProperties = checkNode.getProperties(); + assertTrue("Propery map retrieved was not the same instance", checkProperties == properties); + assertTrue("Property not found", checkProperties.containsKey(ContentModel.PROP_NAME)); +// assertTrue("Property value instance retrieved not the same", checkProperties) + + flushAndClear(); + // commit the transaction + setComplete(); + endTransaction(); + + TransactionService transactionService = (TransactionService) applicationContext.getBean("transactionComponent"); + UserTransaction txn = transactionService.getUserTransaction(); + try + { + txn.begin(); + + // check that the L2 cache hands back the same instance + checkNode = (Node) getSession().get(NodeImpl.class, key); + assertNotNull(checkNode); + checkAspects = checkNode.getAspects(); + +// assertTrue("Node retrieved was not same instance", checkNode == node); + + txn.commit(); + } + catch (Throwable e) + { + txn.rollback(); + } + + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml new file mode 100644 index 0000000000..0a43cd27da --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + store + from + org.alfresco.repo.domain.hibernate.StoreImpl as store + + + + select + assoc + from + org.alfresco.repo.domain.hibernate.NodeImpl as source + join source.targetNodeAssocs as assoc + join assoc.target as target + where + source.key.protocol = :sourceKeyProtocol and + source.key.identifier = :sourceKeyIdentifier and + source.key.guid = :sourceKeyGuid and + assoc.typeQName = :assocTypeQName and + target.key.protocol = :targetKeyProtocol and + target.key.identifier = :targetKeyIdentifier and + target.key.guid = :targetKeyGuid + + + + select + target + from + org.alfresco.repo.domain.hibernate.NodeImpl as source + join source.targetNodeAssocs as assoc + join assoc.target as target + where + source.key.protocol = :sourceKeyProtocol and + source.key.identifier = :sourceKeyIdentifier and + source.key.guid = :sourceKeyGuid and + assoc.typeQName = :assocTypeQName + + + + select + source + from + org.alfresco.repo.domain.hibernate.NodeImpl as target + join target.sourceNodeAssocs as assoc + join assoc.source as source + where + target.key.protocol = :targetKeyProtocol and + target.key.identifier = :targetKeyIdentifier and + target.key.guid = :targetKeyGuid and + assoc.typeQName = :assocTypeQName + + + + select distinct + status.changeTxnId + from + org.alfresco.repo.domain.hibernate.NodeStatusImpl as status + where + status.changeTxnId > :currentTxnId + order by + status.changeTxnId + + + + select + count(status.changeTxnId) + from + org.alfresco.repo.domain.hibernate.NodeStatusImpl as status + where + status.key.protocol = :storeProtocol and + status.key.identifier = :storeIdentifier and + status.deleted = :deleted and + status.changeTxnId = :changeTxnId + + + + select + status + from + org.alfresco.repo.domain.hibernate.NodeStatusImpl as status + where + status.key.protocol = :storeProtocol and + status.key.identifier = :storeIdentifier and + status.deleted = :deleted and + status.changeTxnId = :changeTxnId + + + diff --git a/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java b/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java new file mode 100644 index 0000000000..6362d53ae5 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.NodeAssoc; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; + +/** + * Hibernate-specific implementation of the generic node association + * + * @author Derek Hulley + */ +public class NodeAssocImpl implements NodeAssoc +{ + private long id; + private Node source; + private Node target; + private QName typeQName; + private transient AssociationRef nodeAssocRef; + + public NodeAssocImpl() + { + } + + public void buildAssociation(Node sourceNode, Node targetNode) + { + // add the forward associations + this.setTarget(targetNode); + this.setSource(sourceNode); + // add the inverse associations + sourceNode.getTargetNodeAssocs().add(this); + targetNode.getSourceNodeAssocs().add(this); + } + + public void removeAssociation() + { + // maintain inverse assoc from source node to this instance + this.getSource().getTargetNodeAssocs().remove(this); + // maintain inverse assoc from target node to this instance + this.getTarget().getSourceNodeAssocs().remove(this); + } + + public synchronized AssociationRef getNodeAssocRef() + { + if (nodeAssocRef == null) + { + nodeAssocRef = new AssociationRef(getSource().getNodeRef(), + this.typeQName, + getTarget().getNodeRef()); + } + return nodeAssocRef; + } + + public String toString() + { + StringBuffer sb = new StringBuffer(32); + sb.append("NodeAssoc") + .append("[ source=").append(source) + .append(", target=").append(target) + .append(", name=").append(getTypeQName()) + .append("]"); + return sb.toString(); + } + + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (obj == this) + { + return true; + } + else if (!(obj instanceof NodeAssoc)) + { + return false; + } + NodeAssoc that = (NodeAssoc) obj; + return (EqualsHelper.nullSafeEquals(this.getTypeQName(), that.getTypeQName()) + && EqualsHelper.nullSafeEquals(this.getTarget(), that.getTarget()) + && EqualsHelper.nullSafeEquals(this.getSource(), that.getSource())); + } + + public int hashCode() + { + return (typeQName == null ? 0 : typeQName.hashCode()); + } + + public long getId() + { + return id; + } + + /** + * For Hibernate use + */ + private void setId(long id) + { + this.id = id; + } + + public Node getSource() + { + return source; + } + + /** + * For internal use + */ + private void setSource(Node source) + { + this.source = source; + } + + public Node getTarget() + { + return target; + } + + /** + * For internal use + */ + private void setTarget(Node target) + { + this.target = target; + } + + public QName getTypeQName() + { + return typeQName; + } + + public void setTypeQName(QName typeQName) + { + this.typeQName = typeQName; + } +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java b/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java new file mode 100644 index 0000000000..cfee421637 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.NodeAssoc; +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.repo.domain.PropertyValue; +import org.alfresco.repo.domain.Store; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.hibernate.mapping.Bag; + +/** + * Bean containing all the persistence data representing a node. + *

    + * This implementation of the {@link org.alfresco.repo.domain.Node Node} interface is + * Hibernate specific. + * + * @author Derek Hulley + */ +public class NodeImpl implements Node +{ + private NodeKey key; + private Store store; + private QName typeQName; + private NodeStatus status; + private Set aspects; + private Collection sourceNodeAssocs; + private Collection targetNodeAssocs; + private Collection parentAssocs; + private Collection childAssocs; + private Map properties; + private transient NodeRef nodeRef; + + public NodeImpl() + { + aspects = new HashSet(5); + sourceNodeAssocs = new ArrayList(3); + targetNodeAssocs = new ArrayList(3); + parentAssocs = new ArrayList(3); + childAssocs = new ArrayList(3); + properties = new HashMap(5); + } + + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (obj == this) + { + return true; + } + else if (!(obj instanceof Node)) + { + return false; + } + Node that = (Node) obj; + return (this.getKey().equals(that.getKey())); + } + + public int hashCode() + { + return getKey().hashCode(); + } + + public NodeKey getKey() { + return key; + } + + public void setKey(NodeKey key) { + this.key = key; + } + + public Store getStore() + { + return store; + } + + public synchronized void setStore(Store store) + { + this.store = store; + this.nodeRef = null; + } + + public QName getTypeQName() + { + return typeQName; + } + + public void setTypeQName(QName typeQName) + { + this.typeQName = typeQName; + } + + public NodeStatus getStatus() + { + return status; + } + + public void setStatus(NodeStatus status) + { + this.status = status; + } + + public Set getAspects() + { + return aspects; + } + + /** + * For Hibernate use + */ + private void setAspects(Set aspects) + { + this.aspects = aspects; + } + + public Collection getSourceNodeAssocs() + { + return sourceNodeAssocs; + } + + /** + * For Hibernate use + */ + private void setSourceNodeAssocs(Collection sourceNodeAssocs) + { + this.sourceNodeAssocs = sourceNodeAssocs; + } + + public Collection getTargetNodeAssocs() + { + return targetNodeAssocs; + } + + /** + * For Hibernate use + */ + private void setTargetNodeAssocs(Collection targetNodeAssocs) + { + this.targetNodeAssocs = targetNodeAssocs; + } + + public Collection getParentAssocs() + { + return parentAssocs; + } + + /** + * For Hibernate use + */ + private void setParentAssocs(Collection parentAssocs) + { + this.parentAssocs = parentAssocs; + } + + public Collection getChildAssocs() + { + return childAssocs; + } + + /** + * For Hibernate use + */ + private void setChildAssocs(Collection childAssocs) + { + this.childAssocs = childAssocs; + } + + public Map getProperties() + { + return properties; + } + + /** + * For Hibernate use + */ + private void setProperties(Map properties) + { + this.properties = properties; + } + + /** + * Thread-safe caching of the reference is provided + */ + public synchronized NodeRef getNodeRef() + { + if (nodeRef == null && key != null) + { + nodeRef = new NodeRef(getStore().getStoreRef(), getKey().getGuid()); + } + return nodeRef; + } + + /** + * @see #getNodeRef() + */ + public String toString() + { + return getNodeRef().toString(); + } +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/NodeStatusImpl.java b/source/java/org/alfresco/repo/domain/hibernate/NodeStatusImpl.java new file mode 100644 index 0000000000..a65ba1cbb3 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/NodeStatusImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.util.EqualsHelper; + +/** + * Hibernate implementation of a node status + * + * @author Derek Hulley + */ +public class NodeStatusImpl implements NodeStatus +{ + private NodeKey key; + private String changeTxnId; + private boolean deleted; + + public int hashCode() + { + return (key == null) ? 0 : key.hashCode(); + } + + public boolean equals(Object obj) + { + if (obj == this) + return true; + else if (obj == null) + return false; + else if (!(obj instanceof NodeStatusImpl)) + return false; + NodeStatus that = (NodeStatus) obj; + return (EqualsHelper.nullSafeEquals(this.key, that.getKey())) && + (EqualsHelper.nullSafeEquals(this.changeTxnId, that.getChangeTxnId())) && + (this.deleted == that.isDeleted()); + + } + + public String toString() + { + StringBuilder sb = new StringBuilder(50); + sb.append("NodeStatus") + .append("[key=").append(key) + .append(", txn=").append(changeTxnId) + .append(", deleted=").append(deleted) + .append("]"); + return sb.toString(); + } + + public NodeKey getKey() + { + return key; + } + + public void setKey(NodeKey key) + { + this.key = key; + } + + public String getChangeTxnId() + { + return changeTxnId; + } + + public void setChangeTxnId(String txnId) + { + this.changeTxnId = txnId; + } + + public boolean isDeleted() + { + return deleted; + } + + public void setDeleted(boolean deleted) + { + this.deleted = deleted; + } +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/QNameUserType.java b/source/java/org/alfresco/repo/domain/hibernate/QNameUserType.java new file mode 100644 index 0000000000..289a027398 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/QNameUserType.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; +import org.hibernate.HibernateException; +import org.hibernate.usertype.UserType; + +/** + * Custom type to hide the persistence of {@link org.alfresco.service.namespace.QName qname} + * instances. + * + * @author Derek Hulley + */ +public class QNameUserType implements UserType +{ + private static int[] SQL_TYPES = new int[] {Types.VARCHAR}; + + public Class returnedClass() + { + return QName.class; + } + + /** + * @see #SQL_TYPES + */ + public int[] sqlTypes() + { + return SQL_TYPES; + } + + public boolean isMutable() + { + return false; + } + + public boolean equals(Object x, Object y) throws HibernateException + { + return EqualsHelper.nullSafeEquals(x, y); + } + + public int hashCode(Object x) throws HibernateException + { + return x.hashCode(); + } + + public Object deepCopy(Object value) throws HibernateException + { + // the qname is immutable + return value; + } + + public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException + { + String qnameStr = rs.getString(names[0]); + if (qnameStr == null) + { + return null; + } + else + { + QName qname = QName.createQName(qnameStr); + return qname; + } + } + + public void nullSafeSet(PreparedStatement stmt, Object value, int index) throws HibernateException, SQLException + { + // convert the qname to a string + stmt.setString(index, value.toString()); + } + + public Object replace(Object original, Object target, Object owner) throws HibernateException + { + // qname is immutable + return original; + } + + public Object assemble(Serializable cached, Object owner) throws HibernateException + { + // qname is serializable + return cached; + } + + public Serializable disassemble(Object value) throws HibernateException + { + // qname is serializable + return (QName) value; + } +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/Store.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Store.hbm.xml new file mode 100644 index 0000000000..ae5fddc23a --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/Store.hbm.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/domain/hibernate/StoreImpl.java b/source/java/org/alfresco/repo/domain/hibernate/StoreImpl.java new file mode 100644 index 0000000000..c88c19cedf --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/StoreImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.Store; +import org.alfresco.repo.domain.StoreKey; +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * Hibernate-specific implementation of the domain entity store. + * + * @author Derek Hulley + */ +public class StoreImpl implements Store +{ + private StoreKey key; + private Node rootNode; + private transient StoreRef storeRef; + + public StoreImpl() + { + } + + /** + * @see #getKey() + */ + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (obj == this) + { + return true; + } + else if (!(obj instanceof Node)) + { + return false; + } + Node that = (Node) obj; + return (this.getKey().equals(that.getKey())); + } + + /** + * @see #getKey() + */ + public int hashCode() + { + return getKey().hashCode(); + } + + /** + * @see #getStoreRef()() + */ + public String toString() + { + return getStoreRef().toString(); + } + + public StoreKey getKey() { + return key; + } + + public synchronized void setKey(StoreKey key) { + this.key = key; + this.storeRef = null; + } + + public Node getRootNode() + { + return rootNode; + } + + public void setRootNode(Node rootNode) + { + this.rootNode = rootNode; + } + + /** + * Lazily constructs StoreRef instance referencing this entity + */ + public synchronized StoreRef getStoreRef() + { + if (storeRef == null && key != null) + { + storeRef = new StoreRef(key.getProtocol(), key.getIdentifier()); + } + return storeRef; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/domain/hibernate/VersionCount.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/VersionCount.hbm.xml new file mode 100644 index 0000000000..2d2f6b170e --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/VersionCount.hbm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/domain/hibernate/VersionCountImpl.java b/source/java/org/alfresco/repo/domain/hibernate/VersionCountImpl.java new file mode 100644 index 0000000000..af59bb8656 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/VersionCountImpl.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.domain.hibernate; + +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.StoreKey; +import org.alfresco.repo.domain.VersionCount; +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * Hibernate-specific implementation of the domain entity versioncounter. + * + * @author Derek Hulley + */ +public class VersionCountImpl implements VersionCount +{ + private StoreKey key; + private int versionCount; + private transient StoreRef storeRef; + + public VersionCountImpl() + { + versionCount = 0; + } + + /** + * @see #getKey() + */ + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (obj == this) + { + return true; + } + else if (!(obj instanceof Node)) + { + return false; + } + Node that = (Node) obj; + return (this.getKey().equals(that.getKey())); + } + + /** + * @see #getKey() + */ + public int hashCode() + { + return getKey().hashCode(); + } + + /** + * @see #getKey() + */ + public String toString() + { + return getKey().toString(); + } + + public StoreKey getKey() { + return key; + } + + public synchronized void setKey(StoreKey key) { + this.key = key; + this.storeRef = null; + } + + /** + * For Hibernate use + */ + private void setVersionCount(int versionCount) + { + this.versionCount = versionCount; + } + + public int incrementVersionCount() + { + return ++versionCount; + } + + /** + * Reset back to 0 + */ + public void resetVersionCount() + { + setVersionCount(0); + } + + public int getVersionCount() + { + return versionCount; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/exporter/ACPExportPackageHandler.java b/source/java/org/alfresco/repo/exporter/ACPExportPackageHandler.java new file mode 100644 index 0000000000..439d3fd785 --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/ACPExportPackageHandler.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.view.ExportPackageHandler; +import org.alfresco.service.cmr.view.ExporterException; +import org.alfresco.util.TempFileProvider; + + +/** + * Handler for exporting Repository to ACP (Alfresco Content Package) file + * + * @author David Caruana + */ +public class ACPExportPackageHandler + implements ExportPackageHandler +{ + /** ACP File Extension */ + public final static String ACP_EXTENSION = "acp"; + + protected OutputStream outputStream; + protected File dataFile; + protected File contentDir; + protected File tempDataFile; + protected OutputStream tempDataFileStream; + protected ZipOutputStream zipStream; + protected int iFileCnt = 0; + + + /** + * Construct + * + * @param destDir + * @param zipFile + * @param dataFile + * @param contentDir + */ + public ACPExportPackageHandler(File destDir, File zipFile, File dataFile, File contentDir, boolean overwrite) + { + try + { + // Ensure ACP file has appropriate ACP extension + String zipFilePath = zipFile.getPath(); + if (!zipFilePath.endsWith("." + ACP_EXTENSION)) + { + zipFilePath += "." + ACP_EXTENSION; + } + + File absZipFile = new File(destDir, zipFilePath); + log("Exporting to package zip file " + absZipFile.getAbsolutePath()); + + if (absZipFile.exists()) + { + if (overwrite == false) + { + throw new ExporterException("Package zip file " + absZipFile.getAbsolutePath() + " already exists."); + } + log("Warning: Overwriting existing package zip file " + absZipFile.getAbsolutePath()); + } + + this.outputStream = new FileOutputStream(absZipFile); + this.dataFile = dataFile; + this.contentDir = contentDir; + } + catch (FileNotFoundException e) + { + throw new ExporterException("Failed to create zip file", e); + } + } + + /** + * Construct + * + * @param outputStream + * @param dataFile + * @param contentDir + */ + public ACPExportPackageHandler(OutputStream outputStream, File dataFile, File contentDir) + { + this.outputStream = outputStream; + this.dataFile = dataFile; + this.contentDir = contentDir; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportPackageHandler#startExport() + */ + public void startExport() + { + zipStream = new ZipOutputStream(outputStream); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportPackageHandler#createDataStream() + */ + public OutputStream createDataStream() + { + tempDataFile = TempFileProvider.createTempFile("exportDataStream", ".xml"); + try + { + tempDataFileStream = new FileOutputStream(tempDataFile); + return tempDataFileStream; + } + catch (FileNotFoundException e) + { + throw new ExporterException("Failed to create data file stream", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportStreamHandler#exportStream(java.io.InputStream) + */ + public ContentData exportContent(InputStream content, ContentData contentData) + { + // create zip entry for stream to export + String contentDirPath = contentDir.getPath(); + if (contentDirPath.indexOf(".") != -1) + { + contentDirPath = contentDirPath.substring(0, contentDirPath.indexOf(".")); + } + File file = new File(contentDirPath, "content" + iFileCnt++ + ".bin"); + + try + { + ZipEntry zipEntry = new ZipEntry(file.getPath()); + zipStream.putNextEntry(zipEntry); + + // copy export stream to zip + copyStream(zipStream, content); + } + catch (IOException e) + { + throw new ExporterException("Failed to zip export stream", e); + } + + return new ContentData(file.getPath(), contentData.getMimetype(), contentData.getSize(), contentData.getEncoding()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportPackageHandler#endExport() + */ + public void endExport() + { + // ensure data file has .xml extension + String dataFilePath = dataFile.getPath(); + if (!dataFilePath.endsWith(".xml")) + { + dataFilePath += ".xml"; + } + + // add data file to zip stream + ZipEntry zipEntry = new ZipEntry(dataFilePath); + + try + { + // close data file stream and place temp data file into zip output stream + tempDataFileStream.close(); + zipStream.putNextEntry(zipEntry); + InputStream dataFileStream = new FileInputStream(tempDataFile); + copyStream(zipStream, dataFileStream); + dataFileStream.close(); + } + catch (IOException e) + { + throw new ExporterException("Failed to zip data stream file", e); + } + + try + { + // close zip stream + zipStream.close(); + } + catch(IOException e) + { + throw new ExporterException("Failed to close zip package stream", e); + } + } + + /** + * Log Export Message + * + * @param message message to log + */ + protected void log(String message) + { + } + + /** + * Copy input stream to output stream + * + * @param output output stream + * @param in input stream + * @throws IOException + */ + private void copyStream(OutputStream output, InputStream in) + throws IOException + { + byte[] buffer = new byte[2048 * 10]; + int read = in.read(buffer, 0, 2048 *10); + while (read != -1) + { + output.write(buffer, 0, read); + read = in.read(buffer, 0, 2048 *10); + } + } + +} diff --git a/source/java/org/alfresco/repo/exporter/ChainedExporter.java b/source/java/org/alfresco/repo/exporter/ChainedExporter.java new file mode 100644 index 0000000000..79d61f44ec --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/ChainedExporter.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.namespace.QName; + + +/** + * Exporter that wraps one or more other exporters and invokes them in the provided order. + * + * @author David Caruana + */ +/*package*/ class ChainedExporter + implements Exporter +{ + private Exporter[] exporters; + + + /** + * Construct + * + * @param exporters array of exporters to invoke + */ + /*package*/ ChainedExporter(Exporter[] exporters) + { + List exporterList = new ArrayList(); + for (Exporter exporter : exporters) + { + if (exporter != null) + { + exporterList.add(exporter); + } + } + this.exporters = exporterList.toArray(new Exporter[exporterList.size()]); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#start() + */ + public void start(ExporterContext context) + { + for (Exporter exporter : exporters) + { + exporter.start(context); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNamespace(java.lang.String, java.lang.String) + */ + public void startNamespace(String prefix, String uri) + { + for (Exporter exporter : exporters) + { + exporter.startNamespace(prefix, uri); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNamespace(java.lang.String) + */ + public void endNamespace(String prefix) + { + for (Exporter exporter : exporters) + { + exporter.endNamespace(prefix); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startNode(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.startNode(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endNode(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.endNode(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAspects(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.startAspects(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAspects(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.endAspects(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAspect(NodeRef nodeRef, QName aspect) + { + for (Exporter exporter : exporters) + { + exporter.startAspect(nodeRef, aspect); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAspect(NodeRef nodeRef, QName aspect) + { + for (Exporter exporter : exporters) + { + exporter.endAspect(nodeRef, aspect); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startProperties(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.startProperties(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endProperties(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.endProperties(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startProperty(NodeRef nodeRef, QName property) + { + for (Exporter exporter : exporters) + { + exporter.startProperty(nodeRef, property); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endProperty(NodeRef nodeRef, QName property) + { + for (Exporter exporter : exporters) + { + exporter.endProperty(nodeRef, property); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable) + */ + public void value(NodeRef nodeRef, QName property, Object value) + { + for (Exporter exporter : exporters) + { + exporter.value(nodeRef, property, value); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Collection) + */ + public void value(NodeRef nodeRef, QName property, Collection values) + { + for (Exporter exporter : exporters) + { + exporter.value(nodeRef, property, values); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#content(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.InputStream) + */ + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData) + { + for (Exporter exporter : exporters) + { + exporter.content(nodeRef, property, content, contentData); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAssoc(NodeRef nodeRef, QName assoc) + { + for (Exporter exporter : exporters) + { + exporter.startAssoc(nodeRef, assoc); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAssoc(NodeRef nodeRef, QName assoc) + { + for (Exporter exporter : exporters) + { + exporter.endAssoc(nodeRef, assoc); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAssocs(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.startAssocs(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAssocs(NodeRef nodeRef) + { + for (Exporter exporter : exporters) + { + exporter.endAssocs(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#warning(java.lang.String) + */ + public void warning(String warning) + { + for (Exporter exporter : exporters) + { + exporter.warning(warning); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#end() + */ + public void end() + { + for (Exporter exporter : exporters) + { + exporter.end(); + } + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/exporter/ExporterComponent.java b/source/java/org/alfresco/repo/exporter/ExporterComponent.java new file mode 100644 index 0000000000..5bdedba33d --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/ExporterComponent.java @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.view.ExportPackageHandler; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterException; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.ImporterException; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; + + + +/** + * Default implementation of the Exporter Service. + * + * @author David Caruana + */ +public class ExporterComponent + implements ExporterService +{ + // Supporting services + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + private NodeService nodeService; + private SearchService searchService; + private ContentService contentService; + private DescriptorService descriptorService; + private AuthenticationService authenticationService; + + /** Indent Size */ + private int indentSize = 2; + + + /** + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param searchService the service to perform path searches + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param descriptorService the descriptor service + */ + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + /** + * @param authenticationService the authentication service + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterService#exportView(java.io.OutputStream, org.alfresco.service.cmr.view.ExporterCrawlerParameters, org.alfresco.service.cmr.view.Exporter) + */ + public void exportView(OutputStream viewWriter, ExporterCrawlerParameters parameters, Exporter progress) + { + ParameterCheck.mandatory("View Writer", viewWriter); + + // Construct a basic XML Exporter + Exporter xmlExporter = createXMLExporter(viewWriter); + + // Export + exportView(xmlExporter, parameters, progress); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterService#exportView(org.alfresco.service.cmr.view.ExportPackageHandler, org.alfresco.service.cmr.view.ExporterCrawlerParameters, org.alfresco.service.cmr.view.Exporter) + */ + public void exportView(ExportPackageHandler exportHandler, ExporterCrawlerParameters parameters, Exporter progress) + { + ParameterCheck.mandatory("Stream Handler", exportHandler); + + // create exporter around export handler + exportHandler.startExport(); + OutputStream dataFile = exportHandler.createDataStream(); + Exporter xmlExporter = createXMLExporter(dataFile); + URLExporter urlExporter = new URLExporter(xmlExporter, exportHandler); + + // export + exportView(urlExporter, parameters, progress); + + // end export + exportHandler.endExport(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterService#exportView(org.alfresco.service.cmr.view.Exporter, org.alfresco.service.cmr.view.ExporterCrawler, org.alfresco.service.cmr.view.Exporter) + */ + public void exportView(Exporter exporter, ExporterCrawlerParameters parameters, Exporter progress) + { + ParameterCheck.mandatory("Exporter", exporter); + + ChainedExporter chainedExporter = new ChainedExporter(new Exporter[] {exporter, progress}); + DefaultCrawler crawler = new DefaultCrawler(); + crawler.export(parameters, chainedExporter); + } + + /** + * Create an XML Exporter that exports repository information to the specified + * output stream in xml format. + * + * @param viewWriter the output stream to write to + * @return the xml exporter + */ + private Exporter createXMLExporter(OutputStream viewWriter) + { + // Define output format + OutputFormat format = OutputFormat.createPrettyPrint(); + format.setNewLineAfterDeclaration(false); + format.setIndentSize(indentSize); + format.setEncoding("UTF-8"); + + // Construct an XML Exporter + try + { + XMLWriter writer = new XMLWriter(viewWriter, format); + return new ViewXMLExporter(namespaceService, nodeService, dictionaryService, writer); + } + catch (UnsupportedEncodingException e) + { + throw new ExporterException("Failed to create XML Writer for export", e); + } + } + + + /** + * Responsible for navigating the Repository from specified location and invoking + * the provided exporter call-back for the actual export implementation. + * + * @author David Caruana + */ + private class DefaultCrawler implements ExporterCrawler + { + // Flush threshold + private int flushThreshold = 500; + private int flushCount = 0; + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterCrawler#export(org.alfresco.service.cmr.view.Exporter) + */ + public void export(ExporterCrawlerParameters parameters, Exporter exporter) + { + ExporterContext context = new ExporterContextImpl(parameters); + exporter.start(context); + + // determine if root repository node + NodeRef nodeRef = context.getExportOf(); + boolean rootNode = nodeService.getRootNode(nodeRef.getStoreRef()).equals(nodeRef); + if (parameters.isCrawlSelf() && !rootNode) + { + walkStartNamespaces(parameters, exporter); + walkNode(nodeRef, parameters, exporter); + walkEndNamespaces(parameters, exporter); + } + else + { + // export child nodes only + List childAssocs = nodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef childAssoc : childAssocs) + { + walkStartNamespaces(parameters, exporter); + walkNode(childAssoc.getChildRef(), parameters, exporter); + walkEndNamespaces(parameters, exporter); + } + } + + exporter.end(); + } + + /** + * Call-backs for start of Namespace scope + */ + private void walkStartNamespaces(ExporterCrawlerParameters parameters, Exporter exporter) + { + Collection prefixes = namespaceService.getPrefixes(); + for (String prefix : prefixes) + { + if (!prefix.equals("xml")) + { + String uri = namespaceService.getNamespaceURI(prefix); + exporter.startNamespace(prefix, uri); + } + } + } + + /** + * Call-backs for end of Namespace scope + */ + private void walkEndNamespaces(ExporterCrawlerParameters parameters, Exporter exporter) + { + Collection prefixes = namespaceService.getPrefixes(); + for (String prefix : prefixes) + { + if (!prefix.equals("xml")) + { + exporter.endNamespace(prefix); + } + } + } + + /** + * Navigate a Node. + * + * @param nodeRef the node to navigate + */ + private void walkNode(NodeRef nodeRef, ExporterCrawlerParameters parameters, Exporter exporter) + { + // Export node (but only if it's not excluded from export) + QName type = nodeService.getType(nodeRef); + if (isExcludedURI(parameters.getExcludeNamespaceURIs(), type.getNamespaceURI())) + { + return; + } + + // Do we need to flush? + flushCount++; + if (flushCount > flushThreshold) + { + AlfrescoTransactionSupport.flush(); + flushCount = 0; + } + + exporter.startNode(nodeRef); + + // Export node aspects + exporter.startAspects(nodeRef); + Set aspects = nodeService.getAspects(nodeRef); + for (QName aspect : aspects) + { + if (!isExcludedURI(parameters.getExcludeNamespaceURIs(), aspect.getNamespaceURI())) + { + exporter.startAspect(nodeRef, aspect); + exporter.endAspect(nodeRef, aspect); + } + } + exporter.endAspects(nodeRef); + + // Export node properties + exporter.startProperties(nodeRef); + Map properties = nodeService.getProperties(nodeRef); + for (QName property : properties.keySet()) + { + // filter out properties whose namespace is excluded + if (isExcludedURI(parameters.getExcludeNamespaceURIs(), property.getNamespaceURI())) + { + continue; + } + + // filter out properties whose value is null, if not required + Object value = properties.get(property); + if (!parameters.isCrawlNullProperties() && value == null) + { + continue; + } + + // start export of property + exporter.startProperty(nodeRef, property); + + // get the property type + PropertyDefinition propertyDef = dictionaryService.getProperty(property); + boolean isContentProperty = (propertyDef == null) ? false : propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT); + + if (isContentProperty) + { + // export property of datatype CONTENT + ContentReader reader = contentService.getReader(nodeRef, property); + if (reader == null || reader.exists() == false) + { + exporter.warning("Failed to read content for property " + property + " on node " + nodeRef); + } + else + { + // filter out content if not required + if (parameters.isCrawlContent()) + { + InputStream inputStream = reader.getContentInputStream(); + try + { + exporter.content(nodeRef, property, inputStream, reader.getContentData()); + } + finally + { + try + { + inputStream.close(); + } + catch(IOException e) + { + throw new ExporterException("Failed to export node content for node " + nodeRef, e); + } + } + } + else + { + // skip content values + exporter.content(nodeRef, property, null, null); + } + } + } + else + { + // Export all other datatypes + try + { + if (value instanceof Collection) + { + exporter.value(nodeRef, property, (Collection)value); + } + else + { + exporter.value(nodeRef, property, value); + } + } + catch(TypeConversionException e) + { + exporter.warning("Value of property " + property + " could not be converted to xml string"); + exporter.value(nodeRef, property, properties.get(property).toString()); + } + } + + // end export of property + exporter.endProperty(nodeRef, property); + } + exporter.endProperties(nodeRef); + + // Export node children + if (parameters.isCrawlChildNodes()) + { + exporter.startAssocs(nodeRef); + List childAssocs = nodeService.getChildAssocs(nodeRef); + for (int i = 0; i < childAssocs.size(); i++) + { + ChildAssociationRef childAssoc = childAssocs.get(i); + QName childAssocType = childAssoc.getTypeQName(); + if (isExcludedURI(parameters.getExcludeNamespaceURIs(), childAssocType.getNamespaceURI())) + { + continue; + } + if (i == 0 || childAssocs.get(i - 1).getTypeQName().equals(childAssocType) == false) + { + exporter.startAssoc(nodeRef, childAssocType); + } + if (!isExcludedURI(parameters.getExcludeNamespaceURIs(), childAssoc.getQName().getNamespaceURI())) + { + walkNode(childAssoc.getChildRef(), parameters, exporter); + } + if (i == childAssocs.size() - 1 || childAssocs.get(i + 1).getTypeQName().equals(childAssocType) == false) + { + exporter.endAssoc(nodeRef, childAssocType); + } + } + exporter.endAssocs(nodeRef); + } + + // TODO: Export node associations + + // Signal end of node + exporter.endNode(nodeRef); + } + + /** + * Is the specified URI an excluded URI? + * + * @param uri the URI to test + * @return true => it's excluded from the export + */ + private boolean isExcludedURI(String[] excludeNamespaceURIs, String uri) + { + for (String excludedURI : excludeNamespaceURIs) + { + if (uri.equals(excludedURI)) + { + return true; + } + } + return false; + } + + } + + + /** + * Exporter Context + */ + private class ExporterContextImpl implements ExporterContext + { + private NodeRef exportOf; + private String exportedBy; + private Date exportedDate; + private String exporterVersion; + + /** + * Construct + * + * @param parameters exporter crawler parameters + */ + public ExporterContextImpl(ExporterCrawlerParameters parameters) + { + // get current user performing export + String currentUserName = authenticationService.getCurrentUserName(); + exportedBy = (currentUserName == null) ? "unknown" : currentUserName; + + // get current date + exportedDate = new Date(System.currentTimeMillis()); + + // get export of + exportOf = getNodeRef(parameters.getExportFrom()); + + // get exporter version + exporterVersion = descriptorService.getDescriptor().getVersion(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterContext#getExportedBy() + */ + public String getExportedBy() + { + return exportedBy; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterContext#getExportedDate() + */ + public Date getExportedDate() + { + return exportedDate; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterContext#getExporterVersion() + */ + public String getExporterVersion() + { + return exporterVersion; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExporterContext#getExportOf() + */ + public NodeRef getExportOf() + { + return exportOf; + } + + /** + * Get the Node Ref from the specified Location + * + * @param location the location + * @return the node reference + */ + private NodeRef getNodeRef(Location location) + { + ParameterCheck.mandatory("Location", location); + + // Establish node to import within + NodeRef nodeRef = (location == null) ? null : location.getNodeRef(); + if (nodeRef == null) + { + // If a specific node has not been provided, default to the root + nodeRef = nodeService.getRootNode(location.getStoreRef()); + } + + // Resolve to path within node, if one specified + String path = (location == null) ? null : location.getPath(); + if (path != null && path.length() >0) + { + // Create a valid path and search + List nodeRefs = searchService.selectNodes(nodeRef, path, null, namespaceService, false); + if (nodeRefs.size() == 0) + { + throw new ImporterException("Path " + path + " within node " + nodeRef + " does not exist - the path must resolve to a valid location"); + } + if (nodeRefs.size() > 1) + { + throw new ImporterException("Path " + path + " within node " + nodeRef + " found too many locations - the path must resolve to one location"); + } + nodeRef = nodeRefs.get(0); + } + + // TODO: Check Node actually exists + + return nodeRef; + } + + } + +} diff --git a/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java b/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java new file mode 100644 index 0000000000..434aaacbad --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/ExporterComponentTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */package org.alfresco.repo.exporter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Collection; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.TempFileProvider; +import org.alfresco.util.debug.NodeStoreInspector; + + +public class ExporterComponentTest extends BaseSpringTest +{ + + private NodeService nodeService; + private ExporterService exporterService; + private ImporterService importerService; + private StoreRef storeRef; + private AuthenticationComponent authenticationComponent; + + + @Override + protected void onSetUpInTransaction() throws Exception + { + nodeService = (NodeService)applicationContext.getBean(ServiceRegistry.NODE_SERVICE.getLocalName()); + exporterService = (ExporterService)applicationContext.getBean("exporterComponent"); + importerService = (ImporterService)applicationContext.getBean("importerComponent"); + + // Create the store +// this.storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); +// this.storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "test"); +// this.storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + + + + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + this.storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + } + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + public void testExport() + throws Exception + { + TestProgress testProgress = new TestProgress(); + Location location = new Location(storeRef); + + // import + InputStream test = getClass().getClassLoader().getResourceAsStream("org/alfresco/repo/importer/importercomponent_test.xml"); + InputStreamReader testReader = new InputStreamReader(test, "UTF-8"); + importerService.importView(testReader, location, null, null); + System.out.println(NodeStoreInspector.dumpNodeStore((NodeService)applicationContext.getBean("NodeService"), storeRef)); + + // now export + location.setPath("/system"); + File tempFile = TempFileProvider.createTempFile("xmlexporttest", ".xml"); + OutputStream output = new FileOutputStream(tempFile); + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(location); + exporterService.exportView(output, parameters, testProgress); + output.close(); + } + + + private static class TestProgress + implements Exporter + { + + public void start(ExporterContext exportNodeRef) + { + System.out.println("TestProgress: start"); + } + + public void startNamespace(String prefix, String uri) + { + System.out.println("TestProgress: start namespace prefix = " + prefix + " uri = " + uri); + } + + public void endNamespace(String prefix) + { + System.out.println("TestProgress: end namespace prefix = " + prefix); + } + + public void startNode(NodeRef nodeRef) + { +// System.out.println("TestProgress: start node " + nodeRef); + } + + public void endNode(NodeRef nodeRef) + { +// System.out.println("TestProgress: end node " + nodeRef); + } + + public void startAspect(NodeRef nodeRef, QName aspect) + { +// System.out.println("TestProgress: start aspect " + aspect); + } + + public void endAspect(NodeRef nodeRef, QName aspect) + { +// System.out.println("TestProgress: end aspect " + aspect); + } + + public void startProperty(NodeRef nodeRef, QName property) + { +// System.out.println("TestProgress: start property " + property); + } + + public void endProperty(NodeRef nodeRef, QName property) + { +// System.out.println("TestProgress: end property " + property); + } + + public void value(NodeRef nodeRef, QName property, Object value) + { +// System.out.println("TestProgress: single value " + value); + } + + public void value(NodeRef nodeRef, QName property, Collection values) + { +// System.out.println("TestProgress: multi value " + value); + } + + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData) + { +// System.out.println("TestProgress: content stream "); + } + + public void startAssoc(NodeRef nodeRef, QName assoc) + { +// System.out.println("TestProgress: start association " + assocDef.getName()); + } + + public void endAssoc(NodeRef nodeRef, QName assoc) + { +// System.out.println("TestProgress: end association " + assocDef.getName()); + } + + public void warning(String warning) + { + System.out.println("TestProgress: warning " + warning); + } + + public void end() + { + System.out.println("TestProgress: end"); + } + + public void startProperties(NodeRef nodeRef) + { +// System.out.println("TestProgress: startProperties: " + nodeRef); + } + + public void endProperties(NodeRef nodeRef) + { +// System.out.println("TestProgress: endProperties: " + nodeRef); + } + + public void startAspects(NodeRef nodeRef) + { +// System.out.println("TestProgress: startAspects: " + nodeRef); + } + + public void endAspects(NodeRef nodeRef) + { +// System.out.println("TestProgress: endAspects: " + nodeRef); + } + + public void startAssocs(NodeRef nodeRef) + { +// System.out.println("TestProgress: startAssocs: " + nodeRef); + } + + public void endAssocs(NodeRef nodeRef) + { +// System.out.println("TestProgress: endAssocs: " + nodeRef); + } + + } + +} diff --git a/source/java/org/alfresco/repo/exporter/ExporterCrawler.java b/source/java/org/alfresco/repo/exporter/ExporterCrawler.java new file mode 100644 index 0000000000..47c3efc404 --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/ExporterCrawler.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; + + +/** + * Responsible for crawling Repository contents and invoking the exporter + * with the contents found. + * + * @author David Caruana + * + */ +public interface ExporterCrawler +{ + /** + * Crawl Repository and export items found + * + * @param parameters crawler parameters + * @param exporter exporter to export via + */ + public void export(ExporterCrawlerParameters parameters, Exporter exporter); +} diff --git a/source/java/org/alfresco/repo/exporter/FileExportPackageHandler.java b/source/java/org/alfresco/repo/exporter/FileExportPackageHandler.java new file mode 100644 index 0000000000..1754e66e9d --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/FileExportPackageHandler.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.view.ExportPackageHandler; +import org.alfresco.service.cmr.view.ExporterException; +import org.alfresco.util.TempFileProvider; + + +/** + * Handler for exporting Repository to file system files + * + * @author David Caruana + */ +public class FileExportPackageHandler + implements ExportPackageHandler +{ + protected File contentDir; + protected File absContentDir; + protected File absDataFile; + protected boolean overwrite; + protected OutputStream absDataStream = null; + + /** + * Constuct Handler + * + * @param destDir destination directory + * @param dataFile filename of data file (relative to destDir) + * @param packageDir directory for content (relative to destDir) + * @param overwrite force overwrite of existing package directory + */ + public FileExportPackageHandler(File destDir, File dataFile, File contentDir, boolean overwrite) + { + this.contentDir = contentDir; + this.absContentDir = new File(destDir, contentDir.getPath()); + this.absDataFile = new File(destDir, dataFile.getPath()); + this.overwrite = overwrite; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportPackageHandler#startExport() + */ + public void startExport() + { + log("Exporting to package " + absDataFile.getAbsolutePath()); + + if (absContentDir.exists()) + { + if (overwrite == false) + { + throw new ExporterException("Package content dir " + absContentDir.getAbsolutePath() + " already exists."); + } + log("Warning: Overwriting existing package dir " + absContentDir.getAbsolutePath()); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportPackageHandler#createDataStream() + */ + public OutputStream createDataStream() + { + if (absDataFile.exists()) + { + if (overwrite == false) + { + throw new ExporterException("Package data file " + absDataFile.getAbsolutePath() + " already exists."); + } + log("Warning: Overwriting existing package file " + absDataFile.getAbsolutePath()); + absDataFile.delete(); + } + + try + { + absDataFile.createNewFile(); + absDataStream = new FileOutputStream(absDataFile); + return absDataStream; + } + catch(IOException e) + { + throw new ExporterException("Failed to create package file " + absDataFile.getAbsolutePath() + " due to " + e.getMessage()); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportStreamHandler#exportStream(java.io.InputStream) + */ + public ContentData exportContent(InputStream content, ContentData contentData) + { + // Lazily create package directory + try + { + absContentDir.mkdirs(); + } + catch(SecurityException e) + { + throw new ExporterException("Failed to create package dir " + absContentDir.getAbsolutePath() + " due to " + e.getMessage()); + } + + // Create file in package directory to hold exported content + File outputFile = TempFileProvider.createTempFile("export", ".bin", absContentDir); + + try + { + // Copy exported content from repository to exported file + FileOutputStream outputStream = new FileOutputStream(outputFile); + byte[] buffer = new byte[2048 * 10]; + int read = content.read(buffer, 0, 2048 *10); + while (read != -1) + { + outputStream.write(buffer, 0, read); + read = content.read(buffer, 0, 2048 *10); + } + outputStream.close(); + } + catch(FileNotFoundException e) + { + throw new ExporterException("Failed to create export package file due to " + e.getMessage()); + } + catch(IOException e) + { + throw new ExporterException("Failed to export content due to " + e.getMessage()); + } + + // return relative path to exported content file (relative to xml export file) + File url = new File(contentDir, outputFile.getName()); + return new ContentData(url.getPath(), contentData.getMimetype(), contentData.getSize(), contentData.getEncoding()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ExportPackageHandler#endExport() + */ + public void endExport() + { + // close Export File + if (absDataStream != null) + { + try + { + absDataStream.close(); + } + catch(IOException e) + { + throw new ExporterException("Failed to close package data file " + absDataFile + " due to" + e.getMessage()); + } + } + } + + /** + * Log Export Message + * + * @param message message to log + */ + protected void log(String message) + { + } +} diff --git a/source/java/org/alfresco/repo/exporter/URLExporter.java b/source/java/org/alfresco/repo/exporter/URLExporter.java new file mode 100644 index 0000000000..8eaf5561ec --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/URLExporter.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import java.io.InputStream; +import java.util.Collection; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.ExportPackageHandler; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; + + +/** + * Exporter that transforms content properties to URLs. + * + * All other Repository information is exported using the delegated exporter. + * + * @author David Caruana + */ +/*package*/ class URLExporter + implements Exporter +{ + private Exporter exporter; + private ExportPackageHandler streamHandler; + + + /** + * Construct + * @param exporter exporter to delegate to + * @param streamHandler the handler for transforming content streams to URLs + */ + public URLExporter(Exporter exporter, ExportPackageHandler streamHandler) + { + ParameterCheck.mandatory("Exporter", exporter); + ParameterCheck.mandatory("Stream Handler", streamHandler); + + this.exporter = exporter; + this.streamHandler = streamHandler; + } + + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#start() + */ + public void start(ExporterContext context) + { + exporter.start(context); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNamespace(java.lang.String, java.lang.String) + */ + public void startNamespace(String prefix, String uri) + { + exporter.startNamespace(prefix, uri); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNamespace(java.lang.String) + */ + public void endNamespace(String prefix) + { + exporter.endNamespace(prefix); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startNode(NodeRef nodeRef) + { + exporter.startNode(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endNode(NodeRef nodeRef) + { + exporter.endNode(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAspects(NodeRef nodeRef) + { + exporter.startAspects(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAspects(NodeRef nodeRef) + { + exporter.endAspects(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAspect(NodeRef nodeRef, QName aspect) + { + exporter.startAspect(nodeRef, aspect); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAspect(NodeRef nodeRef, QName aspect) + { + exporter.endAspect(nodeRef, aspect); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startProperties(NodeRef nodeRef) + { + exporter.startProperties(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endProperties(NodeRef nodeRef) + { + exporter.endProperties(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startProperty(NodeRef nodeRef, QName property) + { + exporter.startProperty(nodeRef, property); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endProperty(NodeRef nodeRef, QName property) + { + exporter.endProperty(nodeRef, property); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable) + */ + public void value(NodeRef nodeRef, QName property, Object value) + { + exporter.value(nodeRef, property, value); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Collection) + */ + public void value(NodeRef nodeRef, QName property, Collection values) + { + exporter.value(nodeRef, property, values); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#content(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.InputStream) + */ + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData) + { + // Handle the stream by converting it to a URL and export the URL + ContentData exportedContentData = streamHandler.exportContent(content, contentData); + value(nodeRef, property, exportedContentData.toString()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAssoc(NodeRef nodeRef, QName assoc) + { + exporter.startAssoc(nodeRef, assoc); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAssoc(NodeRef nodeRef, QName assoc) + { + exporter.endAssoc(nodeRef, assoc); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAssocs(NodeRef nodeRef) + { + exporter.startAssocs(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAssocs(NodeRef nodeRef) + { + exporter.endAssocs(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#warning(java.lang.String) + */ + public void warning(String warning) + { + exporter.warning(warning); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#end() + */ + public void end() + { + exporter.end(); + } + +} diff --git a/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java b/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java new file mode 100644 index 0000000000..40d7b97e39 --- /dev/null +++ b/source/java/org/alfresco/repo/exporter/ViewXMLExporter.java @@ -0,0 +1,674 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.exporter; + +import java.io.InputStream; +import java.util.Collection; + +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.cmr.view.ExporterException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + + +/** + * Exporter that exports Repository information to XML (Alfresco Repository View Schema) + * + * @author David Caruana + */ +/*package*/ class ViewXMLExporter + implements Exporter +{ + // Repository View Schema Definitions + private static final String VIEW_LOCALNAME = "view"; + private static final String VALUES_LOCALNAME = "values"; + private static final String VALUE_LOCALNAME = "value"; + private static final String CHILDNAME_LOCALNAME = "childName"; + private static final String ASPECTS_LOCALNAME = "aspects"; + private static final String PROPERTIES_LOCALNAME = "properties"; + private static final String ASSOCIATIONS_LOCALNAME = "associations"; + private static final String DATATYPE_LOCALNAME = "datatype"; + private static final String ISNULL_LOCALNAME = "isNull"; + private static final String METADATA_LOCALNAME = "metadata"; + private static final String EXPORTEDBY_LOCALNAME = "exportBy"; + private static final String EXPORTEDDATE_LOCALNAME = "exportDate"; + private static final String EXPORTERVERSION_LOCALNAME = "exporterVersion"; + private static final String EXPORTOF_LOCALNAME = "exportOf"; + private static QName VIEW_QNAME; + private static QName VALUES_QNAME; + private static QName VALUE_QNAME; + private static QName PROPERTIES_QNAME; + private static QName ASPECTS_QNAME; + private static QName ASSOCIATIONS_QNAME; + private static QName CHILDNAME_QNAME; + private static QName DATATYPE_QNAME; + private static QName ISNULL_QNAME; + private static QName METADATA_QNAME; + private static QName EXPORTEDBY_QNAME; + private static QName EXPORTEDDATE_QNAME; + private static QName EXPORTERVERSION_QNAME; + private static QName EXPORTOF_QNAME; + private static final AttributesImpl EMPTY_ATTRIBUTES = new AttributesImpl(); + + // Service dependencies + private NamespaceService namespaceService; + private NodeService nodeService; + private DictionaryService dictionaryService; + + // View context + private ContentHandler contentHandler; + private Path exportNodePath; + + + /** + * Construct + * + * @param namespaceService namespace service + * @param nodeService node service + * @param contentHandler content handler + */ + ViewXMLExporter(NamespaceService namespaceService, NodeService nodeService, DictionaryService dictionaryService, ContentHandler contentHandler) + { + this.namespaceService = namespaceService; + this.nodeService = nodeService; + this.dictionaryService = dictionaryService; + this.contentHandler = contentHandler; + + VIEW_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, VIEW_LOCALNAME, namespaceService); + VALUE_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, namespaceService); + VALUES_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUES_LOCALNAME, namespaceService); + CHILDNAME_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, CHILDNAME_LOCALNAME, namespaceService); + ASPECTS_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, ASPECTS_LOCALNAME, namespaceService); + PROPERTIES_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, PROPERTIES_LOCALNAME, namespaceService); + ASSOCIATIONS_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, ASSOCIATIONS_LOCALNAME, namespaceService); + DATATYPE_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, DATATYPE_LOCALNAME, namespaceService); + ISNULL_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, ISNULL_LOCALNAME, namespaceService); + METADATA_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, METADATA_LOCALNAME, namespaceService); + EXPORTEDBY_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTEDBY_LOCALNAME, namespaceService); + EXPORTEDDATE_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTEDDATE_LOCALNAME, namespaceService); + EXPORTERVERSION_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTERVERSION_LOCALNAME, namespaceService); + EXPORTOF_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTOF_LOCALNAME, namespaceService); + } + + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#start() + */ + public void start(ExporterContext context) + { + try + { + exportNodePath = nodeService.getPath(context.getExportOf()); + contentHandler.startDocument(); + contentHandler.startPrefixMapping(NamespaceService.REPOSITORY_VIEW_PREFIX, NamespaceService.REPOSITORY_VIEW_1_0_URI); + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VIEW_LOCALNAME, VIEW_QNAME.toPrefixString(), EMPTY_ATTRIBUTES); + + // + // output metadata + // + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, METADATA_LOCALNAME, METADATA_QNAME.toPrefixString(), EMPTY_ATTRIBUTES); + + // exported by + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTEDBY_LOCALNAME, EXPORTEDBY_QNAME.toPrefixString(), EMPTY_ATTRIBUTES); + contentHandler.characters(context.getExportedBy().toCharArray(), 0, context.getExportedBy().length()); + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTEDBY_LOCALNAME, EXPORTEDBY_QNAME.toPrefixString()); + + // exported date + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTEDDATE_LOCALNAME, EXPORTEDDATE_QNAME.toPrefixString(), EMPTY_ATTRIBUTES); + String date = DefaultTypeConverter.INSTANCE.convert(String.class, context.getExportedDate()); + contentHandler.characters(date.toCharArray(), 0, date.length()); + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTEDDATE_LOCALNAME, EXPORTEDDATE_QNAME.toPrefixString()); + + // exporter version + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTERVERSION_LOCALNAME, EXPORTERVERSION_QNAME.toPrefixString(), EMPTY_ATTRIBUTES); + contentHandler.characters(context.getExporterVersion().toCharArray(), 0, context.getExporterVersion().length()); + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTERVERSION_LOCALNAME, EXPORTERVERSION_QNAME.toPrefixString()); + + // export of + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTOF_LOCALNAME, EXPORTOF_QNAME.toPrefixString(), EMPTY_ATTRIBUTES); + String path = exportNodePath.toPrefixString(namespaceService); + contentHandler.characters(path.toCharArray(), 0, path.length()); + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, EXPORTOF_LOCALNAME, EXPORTOF_QNAME.toPrefixString()); + + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, METADATA_LOCALNAME, METADATA_QNAME.toPrefixString()); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process export start event", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNamespace(java.lang.String, java.lang.String) + */ + public void startNamespace(String prefix, String uri) + { + try + { + contentHandler.startPrefixMapping(prefix, uri); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start namespace event - prefix " + prefix + " uri " + uri, e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNamespace(java.lang.String) + */ + public void endNamespace(String prefix) + { + try + { + contentHandler.endPrefixMapping(prefix); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end namespace event - prefix " + prefix, e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startNode(NodeRef nodeRef) + { + try + { + AttributesImpl attrs = new AttributesImpl(); + + Path path = nodeService.getPath(nodeRef); + if (path.size() > 1) + { + // a child name does not exist for root + Path.ChildAssocElement pathElement = (Path.ChildAssocElement)path.last(); + QName childQName = pathElement.getRef().getQName(); + attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_1_0_URI, CHILDNAME_LOCALNAME, CHILDNAME_QNAME.toPrefixString(), null, toPrefixString(childQName)); + } + + QName type = nodeService.getType(nodeRef); + contentHandler.startElement(type.getNamespaceURI(), type.getLocalName(), toPrefixString(type), attrs); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start node event - node ref " + nodeRef.toString(), e); + } + } + + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endNode(NodeRef nodeRef) + { + try + { + QName type = nodeService.getType(nodeRef); + contentHandler.endElement(type.getNamespaceURI(), type.getLocalName(), toPrefixString(type)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end node event - node ref " + nodeRef.toString(), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAspects(NodeRef nodeRef) + { + try + { + contentHandler.startElement(ASPECTS_QNAME.getNamespaceURI(), ASPECTS_LOCALNAME, toPrefixString(ASPECTS_QNAME), EMPTY_ATTRIBUTES); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start aspects", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAspects(NodeRef nodeRef) + { + try + { + contentHandler.endElement(ASPECTS_QNAME.getNamespaceURI(), ASPECTS_LOCALNAME, toPrefixString(ASPECTS_QNAME)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end aspects", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAspect(NodeRef nodeRef, QName aspect) + { + try + { + contentHandler.startElement(aspect.getNamespaceURI(), aspect.getLocalName(), toPrefixString(aspect), EMPTY_ATTRIBUTES); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start aspect event - node ref " + nodeRef.toString() + "; aspect " + toPrefixString(aspect), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAspect(NodeRef nodeRef, QName aspect) + { + try + { + contentHandler.endElement(aspect.getNamespaceURI(), aspect.getLocalName(), toPrefixString(aspect)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end aspect event - node ref " + nodeRef.toString() + "; aspect " + toPrefixString(aspect), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startProperties(NodeRef nodeRef) + { + try + { + contentHandler.startElement(PROPERTIES_QNAME.getNamespaceURI(), PROPERTIES_LOCALNAME, toPrefixString(PROPERTIES_QNAME), EMPTY_ATTRIBUTES); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start properties", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endProperties(NodeRef nodeRef) + { + try + { + contentHandler.endElement(PROPERTIES_QNAME.getNamespaceURI(), PROPERTIES_LOCALNAME, toPrefixString(PROPERTIES_QNAME)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start properties", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startProperty(NodeRef nodeRef, QName property) + { + try + { + contentHandler.startElement(property.getNamespaceURI(), property.getLocalName(), toPrefixString(property), EMPTY_ATTRIBUTES); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start property event - nodeRef " + nodeRef + "; property " + toPrefixString(property), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endProperty(NodeRef nodeRef, QName property) + { + try + { + contentHandler.endElement(property.getNamespaceURI(), property.getLocalName(), toPrefixString(property)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end property event - nodeRef " + nodeRef + "; property " + toPrefixString(property), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable) + */ + public void value(NodeRef nodeRef, QName property, Object value) + { + try + { + // determine data type of value + QName valueDataType = null; + PropertyDefinition propDef = dictionaryService.getProperty(property); + DataTypeDefinition dataTypeDef = (propDef == null) ? null : propDef.getDataType(); + if (dataTypeDef == null || dataTypeDef.getName().equals(DataTypeDefinition.ANY)) + { + dataTypeDef = (value == null) ? null : dictionaryService.getDataType(value.getClass()); + if (dataTypeDef != null) + { + valueDataType = dataTypeDef.getName(); + } + } + + // convert node references to paths + if (value instanceof NodeRef) + { + Path nodeRefPath = createRelativePath(nodeRef, (NodeRef)value); + value = (nodeRefPath == null) ? null : nodeRefPath.toPrefixString(namespaceService); + } + + // output value wrapper if value is null or property data type is ANY + if (value == null || valueDataType != null) + { + AttributesImpl attrs = new AttributesImpl(); + if (value == null) + { + attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, ISNULL_LOCALNAME, ISNULL_QNAME.toPrefixString(), null, "true"); + } + if (valueDataType != null) + { + attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, DATATYPE_LOCALNAME, DATATYPE_QNAME.toPrefixString(), null, toPrefixString(valueDataType)); + } + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME), attrs); + } + + // output value + String strValue = (String)DefaultTypeConverter.INSTANCE.convert(String.class, value); + if (strValue != null) + { + contentHandler.characters(strValue.toCharArray(), 0, strValue.length()); + } + + // output value wrapper if property data type is any + if (value == null || valueDataType != null) + { + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME)); + } + } + catch (SAXException e) + { + throw new ExporterException("Failed to process value event - nodeRef " + nodeRef + "; property " + toPrefixString(property) + "; value " + value, e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Collection) + */ + public void value(NodeRef nodeRef, QName property, Collection values) + { + try + { + PropertyDefinition propDef = dictionaryService.getProperty(property); + DataTypeDefinition dataTypeDef = (propDef == null) ? null : propDef.getDataType(); + + // start collection + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUES_LOCALNAME, toPrefixString(VALUES_QNAME), EMPTY_ATTRIBUTES); + + for (Object value : values) + { + // determine data type of value + QName valueDataType = null; + if (dataTypeDef == null || dataTypeDef.getName().equals(DataTypeDefinition.ANY)) + { + dataTypeDef = (value == null) ? null : dictionaryService.getDataType(value.getClass()); + if (dataTypeDef != null) + { + valueDataType = dataTypeDef.getName(); + } + } + + // output value wrapper with datatype + AttributesImpl attrs = new AttributesImpl(); + if (value == null) + { + attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, ISNULL_LOCALNAME, ISNULL_QNAME.toPrefixString(), null, "true"); + } + if (valueDataType != null) + { + attrs.addAttribute(NamespaceService.REPOSITORY_VIEW_PREFIX, DATATYPE_LOCALNAME, DATATYPE_QNAME.toPrefixString(), null, toPrefixString(valueDataType)); + } + contentHandler.startElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME), attrs); + + // convert node references to paths + if (value instanceof NodeRef) + { + value = createRelativePath(nodeRef, (NodeRef)value).toPrefixString(namespaceService); + } + + // output value + String strValue = (String)DefaultTypeConverter.INSTANCE.convert(String.class, value); + if (strValue != null) + { + contentHandler.characters(strValue.toCharArray(), 0, strValue.length()); + } + + // output value wrapper if property data type is any + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME)); + } + + // end collection + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VALUES_LOCALNAME, toPrefixString(VALUES_QNAME)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process multi-value event - nodeRef " + nodeRef + "; property " + toPrefixString(property), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#content(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.InputStream) + */ + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData) + { + // TODO: Base64 encode content and send out via Content Handler + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAssoc(NodeRef nodeRef, QName assoc) + { + try + { + contentHandler.startElement(assoc.getNamespaceURI(), assoc.getLocalName(), toPrefixString(assoc), EMPTY_ATTRIBUTES); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start assoc event - nodeRef " + nodeRef + "; association " + toPrefixString(assoc), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAssoc(NodeRef nodeRef, QName assoc) + { + try + { + contentHandler.endElement(assoc.getNamespaceURI(), assoc.getLocalName(), toPrefixString(assoc)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end assoc event - nodeRef " + nodeRef + "; association " + toPrefixString(assoc), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAssocs(NodeRef nodeRef) + { + try + { + contentHandler.startElement(ASSOCIATIONS_QNAME.getNamespaceURI(), ASSOCIATIONS_LOCALNAME, toPrefixString(ASSOCIATIONS_QNAME), EMPTY_ATTRIBUTES); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process start associations", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAssocs(NodeRef nodeRef) + { + try + { + contentHandler.endElement(ASSOCIATIONS_QNAME.getNamespaceURI(), ASSOCIATIONS_LOCALNAME, toPrefixString(ASSOCIATIONS_QNAME)); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end associations", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#warning(java.lang.String) + */ + public void warning(String warning) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#end() + */ + public void end() + { + try + { + contentHandler.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, VIEW_LOCALNAME, VIEW_QNAME.toPrefixString()); + contentHandler.endPrefixMapping(NamespaceService.REPOSITORY_VIEW_PREFIX); + contentHandler.endDocument(); + } + catch (SAXException e) + { + throw new ExporterException("Failed to process end export event", e); + } + } + + /** + * Get the prefix for the specified URI + * @param uri the URI + * @return the prefix (or null, if one is not registered) + */ + private String toPrefixString(QName qname) + { + return qname.toPrefixString(namespaceService); + } + + /** + * Return relative path between from and to references within export root + * + * @param fromRef from reference + * @param toRef to reference + * @return path + */ + private Path createRelativePath(NodeRef fromRef, NodeRef toRef) + { + // Check that item exists first + if (!nodeService.exists(toRef)) + { + // return null path + return null; + } + + Path fromPath = nodeService.getPath(fromRef); + Path toPath = nodeService.getPath(toRef); + Path relativePath = null; + + try + { + // Determine if 'to path' is a category + // TODO: This needs to be resolved in a more appropriate manner - special support is + // required for categories. + for (int i = 0; i < toPath.size(); i++) + { + Path.Element pathElement = toPath.get(i); + if (pathElement.getPrefixedString(namespaceService).equals("cm:categoryRoot")) + { + Path.ChildAssocElement childPath = (Path.ChildAssocElement)pathElement; + relativePath = new Path(); + relativePath.append(new Path.ChildAssocElement(new ChildAssociationRef(null, null, null, childPath.getRef().getParentRef()))); + relativePath.append(toPath.subPath(i + 1, toPath.size() -1)); + break; + } + } + + if (relativePath == null) + { + // Determine if from node is relative to export tree + int i = 0; + while (i < exportNodePath.size() && i < fromPath.size() && exportNodePath.get(i).equals(fromPath.get(i))) + { + i++; + } + if (i == exportNodePath.size()) + { + // Determine if to node is relative to export tree + i = 0; + while (i < exportNodePath.size() && i < toPath.size() && exportNodePath.get(i).equals(toPath.get(i))) + { + i++; + } + if (i == exportNodePath.size()) + { + // build relative path between from and to + relativePath = new Path(); + for (int p = 0; p < fromPath.size() - i; p++) + { + relativePath.append(new Path.ParentElement()); + } + if (i < toPath.size()) + { + relativePath.append(toPath.subPath(i, toPath.size() -1)); + } + } + } + } + + if (relativePath == null) + { + // default to absolute path + relativePath = toPath; + } + } + catch(Throwable e) + { + String msg = "Failed to determine relative path: export path=" + exportNodePath + "; from path=" + fromPath + "; to path=" + toPath; + throw new ExporterException(msg, e); + } + + return relativePath; + } + +} diff --git a/source/java/org/alfresco/repo/importer/ACPImportPackageHandler.java b/source/java/org/alfresco/repo/importer/ACPImportPackageHandler.java new file mode 100644 index 0000000000..ad9d171df3 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ACPImportPackageHandler.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.alfresco.service.cmr.view.ImportPackageHandler; +import org.alfresco.service.cmr.view.ImporterException; + + +/** + * Handler for importing Repository content from zip package file + * + * @author David Caruana + */ +public class ACPImportPackageHandler + implements ImportPackageHandler +{ + + protected File file; + protected ZipFile zipFile; + protected String dataFileEncoding; + + + /** + * Constuct Handler + * + * @param sourceDir source directory + * @param packageDir relative directory within source to place exported content + */ + public ACPImportPackageHandler(File zipFile, String dataFileEncoding) + { + this.file = zipFile; + this.dataFileEncoding = dataFileEncoding; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#startImport() + */ + public void startImport() + { + log("Importing from zip file " + file.getAbsolutePath()); + try + { + zipFile = new ZipFile(file); + } + catch(IOException e) + { + throw new ImporterException("Failed to read zip file due to " + e.getMessage(), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#getDataStream() + */ + public Reader getDataStream() + { + try + { + // find data file + InputStream dataStream = null; + Enumeration entries = zipFile.entries(); + while(entries.hasMoreElements()) + { + ZipEntry entry = (ZipEntry)entries.nextElement(); + if (!entry.isDirectory()) + { + if (entry.getName().endsWith(".xml")) + { + dataStream = zipFile.getInputStream(entry); + } + } + } + + // oh dear, there's no data file + if (dataStream == null) + { + throw new ImporterException("Failed to find data file within zip package"); + } + + Reader inputReader = (dataFileEncoding == null) ? new InputStreamReader(dataStream) : new InputStreamReader(dataStream, dataFileEncoding); + return new BufferedReader(inputReader); + } + catch(UnsupportedEncodingException e) + { + throw new ImporterException("Encoding " + dataFileEncoding + " is not supported"); + } + catch(IOException e) + { + throw new ImporterException("Failed to open data file within zip package due to " + e.getMessage()); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportStreamHandler#importStream(java.lang.String) + */ + public InputStream importStream(String content) + { + ZipEntry zipEntry = zipFile.getEntry(content); + if (zipEntry == null) + { + throw new ImporterException("Failed to find content " + content + " within zip package"); + } + + try + { + return zipFile.getInputStream(zipEntry); + } + catch (IOException e) + { + throw new ImporterException("Failed to open content " + content + " within zip package due to " + e.getMessage(), e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#endImport() + */ + public void endImport() + { + } + + /** + * Log Import Message + * + * @param message message to log + */ + protected void log(String message) + { + } + +} + diff --git a/source/java/org/alfresco/repo/importer/DefaultContentHandler.java b/source/java/org/alfresco/repo/importer/DefaultContentHandler.java new file mode 100644 index 0000000000..1ee0c89c42 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/DefaultContentHandler.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.InputStream; + +import org.alfresco.util.ParameterCheck; +import org.xml.sax.Attributes; +import org.xml.sax.ErrorHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + + + +/** + * Base Import Content Handler + */ +public class DefaultContentHandler + implements ImportContentHandler, ErrorHandler +{ + private ImportContentHandler targetHandler = null; + private Importer importer = null; + + + public DefaultContentHandler(ImportContentHandler targetHandler) + { + ParameterCheck.mandatory("targetHandler", targetHandler); + this.targetHandler = targetHandler; + } + + public void setImporter(Importer importer) + { + this.importer = importer; + this.targetHandler.setImporter(importer); + } + + public InputStream importStream(String content) + { + return targetHandler.importStream(content); + } + + public void setDocumentLocator(Locator locator) + { + targetHandler.setDocumentLocator(locator); + } + + public void startDocument() throws SAXException + { + importer.start(); + targetHandler.startDocument(); + } + + public void endDocument() throws SAXException + { + try + { + targetHandler.endDocument(); + } + finally + { + importer.end(); + } + } + + public void startPrefixMapping(String prefix, String uri) throws SAXException + { + targetHandler.startPrefixMapping(prefix, uri); + } + + public void endPrefixMapping(String prefix) throws SAXException + { + targetHandler.endPrefixMapping(prefix); + } + + public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException + { + targetHandler.startElement(uri, localName, qName, atts); + } + + public void endElement(String uri, String localName, String qName) throws SAXException + { + targetHandler.endElement(uri, localName, qName); + } + + public void characters(char[] ch, int start, int length) throws SAXException + { + targetHandler.characters(ch, start, length); + } + + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException + { + targetHandler.ignorableWhitespace(ch, start, length); + } + + public void processingInstruction(String target, String data) throws SAXException + { + targetHandler.processingInstruction(target, data); + } + + public void skippedEntity(String name) throws SAXException + { + targetHandler.skippedEntity(name); + } + + + public void error(SAXParseException exception) throws SAXException + { + try + { + targetHandler.error(exception); + } + finally + { + importer.error(exception); + } + } + + + public void fatalError(SAXParseException exception) throws SAXException + { + try + { + targetHandler.error(exception); + } + finally + { + importer.error(exception); + } + } + + + public void warning(SAXParseException exception) throws SAXException + { + targetHandler.warning(exception); + } + + +} diff --git a/source/java/org/alfresco/repo/importer/FileImportPackageHandler.java b/source/java/org/alfresco/repo/importer/FileImportPackageHandler.java new file mode 100644 index 0000000000..9dfbcd61d2 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/FileImportPackageHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +import org.alfresco.service.cmr.view.ImportPackageHandler; +import org.alfresco.service.cmr.view.ImporterException; + + +/** + * Handler for importing Repository content streams from file system + * + * @author David Caruana + */ +public class FileImportPackageHandler + implements ImportPackageHandler +{ + protected File sourceDir; + protected File dataFile; + protected String dataFileEncoding; + + /** + * Construct + * + * @param sourceDir + * @param dataFile + * @param dataFileEncoding + */ + public FileImportPackageHandler(File sourceDir, File dataFile, String dataFileEncoding) + { + this.sourceDir = sourceDir; + this.dataFile = new File(sourceDir, dataFile.getPath()); + this.dataFileEncoding = dataFileEncoding; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#startImport() + */ + public void startImport() + { + log("Importing from package " + dataFile.getAbsolutePath()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#getDataStream() + */ + public Reader getDataStream() + { + try + { + InputStream inputStream = new FileInputStream(dataFile); + Reader inputReader = (dataFileEncoding == null) ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, dataFileEncoding); + return new BufferedReader(inputReader); + } + catch(UnsupportedEncodingException e) + { + throw new ImporterException("Encoding " + dataFileEncoding + " is not supported"); + } + catch(IOException e) + { + throw new ImporterException("Failed to read package " + dataFile.getAbsolutePath() + " due to " + e.getMessage()); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportStreamHandler#importStream(java.lang.String) + */ + public InputStream importStream(String content) + { + File fileURL = new File(content); + if (fileURL.isAbsolute() == false) + { + fileURL = new File(sourceDir, content); + } + + try + { + return new FileInputStream(fileURL); + } + catch(IOException e) + { + throw new ImporterException("Failed to read content url " + content + " from file " + fileURL.getAbsolutePath()); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#endImport() + */ + public void endImport() + { + } + + /** + * Log Import Message + * + * @param message message to log + */ + protected void log(String message) + { + } + +} + diff --git a/source/java/org/alfresco/repo/importer/FileImporter.java b/source/java/org/alfresco/repo/importer/FileImporter.java new file mode 100644 index 0000000000..c3333ad9d5 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/FileImporter.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.File; +import java.io.FileFilter; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Interface to load files and directories into the hub. + * All will be created as new - there is no detection if a file exists or has changed etc.. + * + * @author andyh + */ +public interface FileImporter +{ + /** + * Load a file or directory into the repository + * + * @param container - the node into which to insert the file or directory + * @param file - the start point for the import + * @param recurse - if the start point is a directoty then recurse + * @return Returns the number of successfully imported files and directories + * @throws FileImporterException + */ + public int loadFile(NodeRef container, File file, boolean recurse) throws FileImporterException; + + /** + * Load all files or directories that match the file filter in the given directory + * + * @param container + * @param file + * @param filter + * @param recurse + * @return Returns the number of successfully imported files and directories + * @throws FileImporterException + */ + public int loadFile(NodeRef container, File file, FileFilter filter, boolean recurse) throws FileImporterException; + + + /** + * Load a single file or directory without any recursion + * + * @param container + * @param file + * @return Returns the number of successfully imported files and directories + * @throws FileImporterException + */ + public int loadFile(NodeRef container, File file) throws FileImporterException; + + public int loadNamedFile(NodeRef container, File file, boolean recurse, String name) throws FileImporterException; +} diff --git a/source/java/org/alfresco/repo/importer/FileImporterException.java b/source/java/org/alfresco/repo/importer/FileImporterException.java new file mode 100644 index 0000000000..3d94994007 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/FileImporterException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class FileImporterException extends AlfrescoRuntimeException +{ + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 3544669594364490545L; + + public FileImporterException(String msg) + { + super(msg); + } + + public FileImporterException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/repo/importer/FileImporterImpl.java b/source/java/org/alfresco/repo/importer/FileImporterImpl.java new file mode 100644 index 0000000000..41e865044e --- /dev/null +++ b/source/java/org/alfresco/repo/importer/FileImporterImpl.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Simple import of content into the repository + * + * @author andyh + */ +public class FileImporterImpl implements FileImporter +{ + private static Log logger = LogFactory.getLog(FileImporterImpl.class); + + private AuthenticationService authenticationService; + private NodeService nodeService; + private DictionaryService dictionaryService; + private ContentService contentService; + private MimetypeService mimetypeService; + + public FileImporterImpl() + { + super(); + } + + public int loadFile(NodeRef container, File file, boolean recurse) throws FileImporterException + { + Counter counter = new Counter(); + create(counter, container, file, null, recurse, null); + return counter.getCount(); + } + + public int loadNamedFile(NodeRef container, File file, boolean recurse, String name) throws FileImporterException + { + Counter counter = new Counter(); + create(counter, container, file, null, recurse, name); + return counter.getCount(); + } + + public int loadFile(NodeRef container, File file, FileFilter filter, boolean recurse) throws FileImporterException + { + Counter counter = new Counter(); + create(counter, container, file, filter, recurse, null); + return counter.getCount(); + } + + public int loadFile(NodeRef container, File file) throws FileImporterException + { + Counter counter = new Counter(); + create(counter, container, file, null, false, null); + return counter.getCount(); + } + + /** Helper class for mutable int */ + private static class Counter + { + private int count = 0; + public void increment() + { + count++; + } + public int getCount() + { + return count; + } + } + + private NodeRef create(Counter counter, NodeRef container, File file, FileFilter filter, boolean recurse, String containerName) + { + if(containerName != null) + { + NodeRef newContainer = createDirectory(container, containerName, containerName); + return create(counter, newContainer, file, filter, recurse, null); + + } + if (file.isDirectory()) + { + NodeRef directoryNodeRef = createDirectory(container, file); + counter.increment(); + + if(recurse) + { + File[] files = ((filter == null) ? file.listFiles() : file.listFiles(filter)); + for(int i = 0; i < files.length; i++) + { + create(counter, directoryNodeRef, files[i], filter, recurse, null); + } + } + + return directoryNodeRef; + } + else + { + counter.increment(); + return createFile(container, file); + } + } + + /** + * Get the type of child association that should be created. + * + * @param parentNodeRef the parent + * @return Returns the appropriate child association type qualified name for the type of the + * parent. Null will be returned if it can't be determined. + */ + private QName getAssocTypeQName(NodeRef parentNodeRef) + { + // check the parent node's type to determine which association to use + QName parentNodeTypeQName = nodeService.getType(parentNodeRef); + QName assocTypeQName = null; + if (dictionaryService.isSubClass(parentNodeTypeQName, ContentModel.TYPE_CONTAINER)) + { + // it may be a root node or something similar + assocTypeQName = ContentModel.ASSOC_CHILDREN; + } + else if (dictionaryService.isSubClass(parentNodeTypeQName, ContentModel.TYPE_FOLDER)) + { + // more like a directory + assocTypeQName = ContentModel.ASSOC_CONTAINS; + } + return assocTypeQName; + } + + private NodeRef createFile(NodeRef parentNodeRef, File file) + { + // check the parent node's type to determine which association to use + QName assocTypeQName = getAssocTypeQName(parentNodeRef); + if (assocTypeQName == null) + { + throw new IllegalArgumentException( + "Unable to create file. " + + "Parent type is inappropriate: " + nodeService.getType(parentNodeRef)); + } + + // create properties for content type + Map contentProps = new HashMap(3, 1.0f); + contentProps.put(ContentModel.PROP_NAME, file.getName()); + contentProps.put( + ContentModel.PROP_CONTENT, + new ContentData(null, mimetypeService.guessMimetype(file.getName()), 0L, "UTF-8")); + String currentUser = authenticationService.getCurrentUserName(); + contentProps.put(ContentModel.PROP_CREATOR, currentUser == null ? "unknown" : currentUser); + + // create the node to represent the node + String assocName = QName.createValidLocalName(file.getName()); + ChildAssociationRef assocRef = this.nodeService.createNode( + parentNodeRef, + assocTypeQName, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, assocName), + ContentModel.TYPE_CONTENT, contentProps); + + NodeRef fileNodeRef = assocRef.getChildRef(); + + + if (logger.isDebugEnabled()) + logger.debug("Created file node for file: " + file.getName()); + + // apply the titled aspect - title and description + Map titledProps = new HashMap(5); + titledProps.put(ContentModel.PROP_TITLE, file.getName()); + + titledProps.put(ContentModel.PROP_DESCRIPTION, file.getPath()); + + this.nodeService.addAspect(fileNodeRef, ContentModel.ASPECT_TITLED, titledProps); + + if (logger.isDebugEnabled()) + logger.debug("Added titled aspect with properties: " + titledProps); + + // get a writer for the content and put the file + ContentWriter writer = contentService.getWriter(fileNodeRef, ContentModel.PROP_CONTENT, true); + try + { + writer.putContent(new BufferedInputStream(new FileInputStream(file))); + } + catch (ContentIOException e) + { + throw new FileImporterException("Failed to load content from "+file.getPath(), e); + } + catch (FileNotFoundException e) + { + throw new FileImporterException("Failed to load content (file not found) "+file.getPath(), e); + } + + return fileNodeRef; + } + + private NodeRef createDirectory(NodeRef parentNodeRef, File file) + { + return createDirectory(parentNodeRef, file.getName(), file.getPath()); + + } + + private NodeRef createDirectory(NodeRef parentNodeRef, String name, String path) + { + // check the parent node's type to determine which association to use + QName assocTypeQName = getAssocTypeQName(parentNodeRef); + if (assocTypeQName == null) + { + throw new IllegalArgumentException( + "Unable to create directory. " + + "Parent type is inappropriate: " + nodeService.getType(parentNodeRef)); + } + + String qname = QName.createValidLocalName(name); + ChildAssociationRef assocRef = this.nodeService.createNode( + parentNodeRef, + assocTypeQName, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, qname), + ContentModel.TYPE_FOLDER); + + NodeRef nodeRef = assocRef.getChildRef(); + + // set the name property on the node + this.nodeService.setProperty(nodeRef, ContentModel.PROP_NAME, name); + + if (logger.isDebugEnabled()) + logger.debug("Created folder node with name: " + name); + + // apply the uifacets aspect - icon, title and description props + Map uiFacetsProps = new HashMap(5); + uiFacetsProps.put(ContentModel.PROP_ICON, "space-icon-default"); + uiFacetsProps.put(ContentModel.PROP_TITLE, name); + uiFacetsProps.put(ContentModel.PROP_DESCRIPTION, path); + this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_UIFACETS, uiFacetsProps); + + if (logger.isDebugEnabled()) + logger.debug("Added uifacets aspect with properties: " + uiFacetsProps); + + return nodeRef; + } + + protected void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + protected void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + protected void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + protected void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } +} diff --git a/source/java/org/alfresco/repo/importer/FileImporterTest.java b/source/java/org/alfresco/repo/importer/FileImporterTest.java new file mode 100644 index 0000000000..8ad348cb93 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/FileImporterTest.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.File; +import java.io.FileFilter; +import java.net.URL; +import java.util.List; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.TempFileProvider; +import org.springframework.context.ApplicationContext; + +public class FileImporterTest extends TestCase +{ + static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private NodeService nodeService; + private SearchService searchService; + private DictionaryService dictionaryService; + private ContentService contentService; + private AuthenticationService authenticationService; + private AuthenticationComponent authenticationComponent; + private MimetypeService mimetypeService; + private NamespaceService namespaceService; + + private ServiceRegistry serviceRegistry; + private NodeRef rootNodeRef; + + private SearchService searcher; + + public FileImporterTest() + { + super(); + } + + public FileImporterTest(String arg0) + { + super(arg0); + } + + public void setUp() + { + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + + searcher = serviceRegistry.getSearchService(); + nodeService = serviceRegistry.getNodeService(); + searchService = serviceRegistry.getSearchService(); + dictionaryService = serviceRegistry.getDictionaryService(); + contentService = serviceRegistry.getContentService(); + authenticationService = (AuthenticationService) ctx.getBean("authenticationService"); + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + mimetypeService = serviceRegistry.getMimetypeService(); + namespaceService = serviceRegistry.getNamespaceService(); + + authenticationComponent.setSystemUserAsCurrentUser(); + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + } + + private FileImporter createFileImporter() + { + FileImporterImpl fileImporter = new FileImporterImpl(); + fileImporter.setAuthenticationService(authenticationService); + fileImporter.setContentService(contentService); + fileImporter.setMimetypeService(mimetypeService); + fileImporter.setNodeService(nodeService); + fileImporter.setDictionaryService(dictionaryService); + return fileImporter; + } + + public void testCreateFile() throws Exception + { + FileImporter fileImporter = createFileImporter(); + File file = AbstractContentTransformerTest.loadQuickTestFile("xml"); + fileImporter.loadFile(rootNodeRef, file); + } + + public void testLoadRootNonRecursive1() + { + FileImporter fileImporter = createFileImporter(); + URL url = this.getClass().getClassLoader().getResource(""); + File root = new File(url.getFile()); + int count = fileImporter.loadFile(rootNodeRef, new File(url.getFile())); + assertEquals("Expected to load a single file", 1, count); + } + + public void testLoadRootNonRecursive2() + { + FileImporter fileImporter = createFileImporter(); + URL url = this.getClass().getClassLoader().getResource(""); + File root = new File(url.getFile()); + int count = fileImporter.loadFile(rootNodeRef, root, null, false); + assertEquals("Expected to load a single file", 1, count); + } + + public void testLoadXMLFiles() + { + FileImporter fileImporter = createFileImporter(); + URL url = this.getClass().getClassLoader().getResource(""); + FileFilter filter = new XMLFileFilter(); + fileImporter.loadFile(rootNodeRef, new File(url.getFile()), filter, true); + } + + public void testLoadSourceTestResources() + { + FileImporter fileImporter = createFileImporter(); + URL url = this.getClass().getClassLoader().getResource("quick"); + FileFilter filter = new QuickFileFilter(); + fileImporter.loadFile(rootNodeRef, new File(url.getFile()), filter, true); + } + + private static class XMLFileFilter implements FileFilter + { + public boolean accept(File file) + { + return file.getName().endsWith(".xml"); + } + } + + private static class QuickFileFilter implements FileFilter + { + public boolean accept(File file) + { + return file.getName().startsWith("quick"); + } + } + + /** + * @param args + *

      + *
    1. StoreRef + *
    2. Store Path + *
    3. Directory + *
    4. Optional maximum time in seconds for node loading + *
    + * @throws SystemException + * @throws NotSupportedException + * @throws HeuristicRollbackException + * @throws HeuristicMixedException + * @throws RollbackException + * @throws IllegalStateException + * @throws SecurityException + */ + public static final void main(String[] args) throws Exception + { + + int exitCode = 0; + + int grandTotal = 0; + int count = 0; + int target = Integer.parseInt(args[4]); + while (count < target) + { + File directory = TempFileProvider.getTempDir(); + File[] files = directory.listFiles(); + System.out.println("Start temp file count = " + files.length); + + count++; + FileImporterTest test = new FileImporterTest(); + test.setUp(); + + test.authenticationComponent.setSystemUserAsCurrentUser(); + TransactionService transactionService = test.serviceRegistry.getTransactionService(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + + try + { + StoreRef spacesStore = new StoreRef(args[0]); + if (!test.nodeService.exists(spacesStore)) + { + test.nodeService.createStore(spacesStore.getProtocol(), spacesStore.getIdentifier()); + } + + NodeRef storeRoot = test.nodeService.getRootNode(spacesStore); + List location = test.searchService.selectNodes( + storeRoot, + args[1], + null, + test.namespaceService, + false); + if (location.size() == 0) + { + throw new AlfrescoRuntimeException( + "Root node not found, " + + args[1] + + " not found in store, " + + storeRoot); + } + + long start = System.nanoTime(); + int importCount = test.createFileImporter().loadNamedFile(location.get(0), new File(args[2]), true, args[3]+count); + grandTotal += importCount; + long end = System.nanoTime(); + long first = end-start; + System.out.println("Created in: " + ((end - start) / 1000000.0) + "ms"); + start = System.nanoTime(); + + tx.commit(); + end = System.nanoTime(); + long second = end-start; + System.out.println("Committed in: " + ((end - start) / 1000000.0) + "ms"); + double total = ((first+second)/1000000.0); + System.out.println("Grand Total: "+ grandTotal); + System.out.println("Count: "+ count + "ms"); + System.out.println("Imported: " + importCount + " files or directories"); + System.out.println("Average: " + (importCount / (total / 1000.0)) + " files per second"); + + directory = TempFileProvider.getTempDir(); + files = directory.listFiles(); + System.out.println("End temp file count = " + files.length); + + + tx = transactionService.getUserTransaction(); + tx.begin(); + SearchParameters sp = new SearchParameters(); + sp.setLanguage("lucene"); + sp.setQuery("ISNODE:T"); + sp.addStore(spacesStore); + start = System.nanoTime(); + ResultSet rs = test.searchService.query(sp); + end = System.nanoTime(); + System.out.println("Find all in: " + ((end - start) / 1000000.0) + "ms"); + System.out.println(" = "+rs.length() +"\n\n"); + rs.close(); + + sp = new SearchParameters(); + sp.setLanguage("lucene"); + sp.setQuery("TEXT:\"andy\""); + sp.addStore(spacesStore); + start = System.nanoTime(); + rs = test.searchService.query(sp); + end = System.nanoTime(); + System.out.println("Find andy in: " + ((end - start) / 1000000.0) + "ms"); + System.out.println(" = "+rs.length() +"\n\n"); + rs.close(); + + sp = new SearchParameters(); + sp.setLanguage("lucene"); + sp.setQuery("TYPE:\"" + ContentModel.TYPE_CONTENT.toString() + "\""); + sp.addStore(spacesStore); + start = System.nanoTime(); + rs = test.searchService.query(sp); + end = System.nanoTime(); + System.out.println("Find content in: " + ((end - start) / 1000000.0) + "ms"); + System.out.println(" = "+rs.length() +"\n\n"); + rs.close(); + + sp = new SearchParameters(); + sp.setLanguage("lucene"); + sp.setQuery("PATH:\"/*/*/*\""); + sp.addStore(spacesStore); + start = System.nanoTime(); + rs = test.searchService.query(sp); + end = System.nanoTime(); + System.out.println("Find /*/*/* in: " + ((end - start) / 1000000.0) + "ms"); + System.out.println(" = "+rs.length() +"\n\n"); + rs.close(); + + tx.commit(); + + } + catch (Throwable e) + { + tx.rollback(); + e.printStackTrace(); + exitCode = 1; + } + //System.exit(exitCode); + } + System.exit(0); + } +} diff --git a/source/java/org/alfresco/repo/importer/ImportContentHandler.java b/source/java/org/alfresco/repo/importer/ImportContentHandler.java new file mode 100644 index 0000000000..c78970ca0c --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ImportContentHandler.java @@ -0,0 +1,15 @@ +package org.alfresco.repo.importer; + +import java.io.InputStream; + +import org.xml.sax.ContentHandler; +import org.xml.sax.ErrorHandler; + + +public interface ImportContentHandler extends ContentHandler, ErrorHandler +{ + public void setImporter(Importer importer); + + public InputStream importStream(String content); + +} diff --git a/source/java/org/alfresco/repo/importer/ImportNode.java b/source/java/org/alfresco/repo/importer/ImportNode.java new file mode 100644 index 0000000000..1edd035c08 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ImportNode.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Description of node to import. + * + * @author David Caruana + * + */ +public interface ImportNode +{ + /** + * @return the parent context + */ + public ImportParent getParentContext(); + + /** + * @return the type definition + */ + public TypeDefinition getTypeDefinition(); + + /** + * @return the node ref + */ + public NodeRef getNodeRef(); + + /** + * @return the child name + */ + public String getChildName(); + + /** + * Gets all properties for the node + * + * @return the properties + */ + public Map getProperties(); + + /** + * Gets all property datatypes for the node + * + * @return the property datatypes + */ + public Map getPropertyDatatypes(); + + /** + * @return the aspects of this node + */ + public Set getNodeAspects(); + +} diff --git a/source/java/org/alfresco/repo/importer/ImportParent.java b/source/java/org/alfresco/repo/importer/ImportParent.java new file mode 100644 index 0000000000..a83f060307 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ImportParent.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Description of parent for node to import. + * + * @author David Caruana + * + */ +public interface ImportParent +{ + /** + * @return the parent ref + */ + /*package*/ NodeRef getParentRef(); + + /** + * @return the child association type + */ + /*package*/ QName getAssocType(); + +} diff --git a/source/java/org/alfresco/repo/importer/Importer.java b/source/java/org/alfresco/repo/importer/Importer.java new file mode 100644 index 0000000000..896c03b7e1 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/Importer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * The Importer interface encapusulates the strategy for importing + * a node into the Repository. + * + * @author David Caruana + */ +public interface Importer +{ + /** + * @return the root node to import into + */ + public NodeRef getRootRef(); + + /** + * @return the root child association type to import under + */ + public QName getRootAssocType(); + + /** + * Signal start of import + */ + public void start(); + + /** + * Signal end of import + */ + public void end(); + + /** + * Signal import error + */ + public void error(Throwable e); + + /** + * Import meta-data + */ + public void importMetaData(Map properties); + + /** + * Import a node + * + * @param node the node description + * @return the node ref of the imported node + */ + public NodeRef importNode(ImportNode node); + + /** + * Signal completion of node import + * + * @param nodeRef the node ref of the imported node + */ + public void childrenImported(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/repo/importer/ImporterBootstrap.java b/source/java/org/alfresco/repo/importer/ImporterBootstrap.java new file mode 100644 index 0000000000..e6b35d0fb8 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ImporterBootstrap.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.StringTokenizer; + +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ImporterBinding; +import org.alfresco.service.cmr.view.ImporterException; +import org.alfresco.service.cmr.view.ImporterProgress; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Bootstrap Repository store. + * + * @author David Caruana + */ +public class ImporterBootstrap +{ + // View Properties (used in setBootstrapViews) + public static final String VIEW_PATH_PROPERTY = "path"; + public static final String VIEW_CHILDASSOCTYPE_PROPERTY = "childAssocType"; + public static final String VIEW_MESSAGES_PROPERTY = "messages"; + public static final String VIEW_LOCATION_VIEW = "location"; + public static final String VIEW_ENCODING = "encoding"; + + // Logger + private static final Log logger = LogFactory.getLog(ImporterBootstrap.class); + + // Dependencies + private boolean allowWrite = true; + private TransactionService transactionService; + private NamespaceService namespaceService; + private NodeService nodeService; + private ImporterService importerService; + private List bootstrapViews; + private StoreRef storeRef = null; + private List mustNotExistStoreUrls = null; + private Properties configuration = null; + private String strLocale = null; + private Locale locale = null; + private AuthenticationComponent authenticationComponent; + + /** + * Set whether we write or not + * + * @param write true (default) if the import must go ahead, otherwise no import will occur + */ + public void setAllowWrite(boolean write) + { + this.allowWrite = write; + } + + /** + * Sets the Transaction Service + * + * @param userTransaction the transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Sets the namespace service + * + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Sets the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the importer service + * + * @param importerService the importer service + */ + public void setImporterService(ImporterService importerService) + { + this.importerService = importerService; + } + + /** + * Set the authentication component + * + * @param authenticationComponent + */ + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + /** + * Sets the bootstrap views + * + * @param bootstrapViews + */ + public void setBootstrapViews(List bootstrapViews) + { + this.bootstrapViews = bootstrapViews; + } + + /** + * Sets the Store Ref to bootstrap into + * + * @param storeUrl + */ + public void setStoreUrl(String storeUrl) + { + this.storeRef = new StoreRef(storeUrl); + } + + /** + * If any of the store urls exist, the bootstrap does not take place + * + * @param storeUrls the list of store urls to check + */ + public void setMustNotExistStoreUrls(List storeUrls) + { + this.mustNotExistStoreUrls = storeUrls; + } + + /** + * Gets the Store Reference + * + * @return store reference + */ + public StoreRef getStoreRef() + { + return this.storeRef; + } + + /** + * Sets the Configuration values for binding place holders + * + * @param configuration + */ + public void setConfiguration(Properties configuration) + { + this.configuration = configuration; + } + + /** + * Gets the Configuration values for binding place holders + * + * @return configuration + */ + public Properties getConfiguration() + { + return configuration; + } + + /** + * Sets the Locale + * + * @param locale (language_country_variant) + */ + public void setLocale(String locale) + { + // construct locale + StringTokenizer t = new StringTokenizer(locale, "_"); + int tokens = t.countTokens(); + if (tokens == 1) + { + this.locale = new Locale(locale); + } + else if (tokens == 2) + { + this.locale = new Locale(t.nextToken(), t.nextToken()); + } + else if (tokens == 3) + { + this.locale = new Locale(t.nextToken(), t.nextToken(), t.nextToken()); + } + + // store original + strLocale = locale; + } + + /** + * Get Locale + * + * @return locale + */ + public String getLocale() + { + return strLocale; + } + + /** + * Boostrap the Repository + */ + public void bootstrap() + { + if (transactionService == null) + { + throw new ImporterException("Transaction Service must be provided"); + } + if (namespaceService == null) + { + throw new ImporterException("Namespace Service must be provided"); + } + if (nodeService == null) + { + throw new ImporterException("Node Service must be provided"); + } + if (importerService == null) + { + throw new ImporterException("Importer Service must be provided"); + } + if (storeRef == null) + { + throw new ImporterException("Store URL must be provided"); + } + + UserTransaction userTransaction = transactionService.getUserTransaction(); + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + + try + { + userTransaction.begin(); + + // check the repository exists, create if it doesn't + if (!performBootstrap()) + { + if (logger.isDebugEnabled()) + logger.debug("Store exists - bootstrap ignored: " + storeRef); + + userTransaction.rollback(); + } + else if (!allowWrite) + { + // we're in read-only node + logger.warn("Store does not exist, but mode is read-only: " + storeRef); + userTransaction.rollback(); + } + else + { + // create the store + storeRef = nodeService.createStore(storeRef.getProtocol(), storeRef.getIdentifier()); + + if (logger.isDebugEnabled()) + logger.debug("Created store: " + storeRef); + + // bootstrap the store contents + if (bootstrapViews != null) + { + for (Properties bootstrapView : bootstrapViews) + { + // Create input stream reader onto view file + String view = bootstrapView.getProperty(VIEW_LOCATION_VIEW); + if (view == null || view.length() == 0) + { + throw new ImporterException("View file location must be provided"); + } + String encoding = bootstrapView.getProperty(VIEW_ENCODING); + Reader viewReader = getReader(view, encoding); + + // Create import location + Location importLocation = new Location(storeRef); + String path = bootstrapView.getProperty(VIEW_PATH_PROPERTY); + if (path != null && path.length() > 0) + { + importLocation.setPath(path); + } + String childAssocType = bootstrapView.getProperty(VIEW_CHILDASSOCTYPE_PROPERTY); + if (childAssocType != null && childAssocType.length() > 0) + { + importLocation.setChildAssocType(QName.createQName(childAssocType, namespaceService)); + } + + // Create import binding + BootstrapBinding binding = new BootstrapBinding(); + binding.setConfiguration(configuration); + String messages = bootstrapView.getProperty(VIEW_MESSAGES_PROPERTY); + if (messages != null && messages.length() > 0) + { + Locale bindingLocale = (locale == null) ? I18NUtil.getLocale() : locale; + ResourceBundle bundle = ResourceBundle.getBundle(messages, bindingLocale); + binding.setResourceBundle(bundle); + } + + // Now import... + importerService.importView(viewReader, importLocation, binding, new BootstrapProgress()); + } + } + + userTransaction.commit(); + } + } + catch(Throwable e) + { + // rollback the transaction + try { if (userTransaction != null) {userTransaction.rollback();} } catch (Exception ex) {} + try {authenticationComponent.clearCurrentSecurityContext(); } catch (Exception ex) {} + throw new AlfrescoRuntimeException("Bootstrap failed", e); + } + finally + { + authenticationComponent.clearCurrentSecurityContext(); + } + } + + /** + * Get a Reader onto an XML view + * + * @param view the view location + * @param encoding the encoding of the view + * @return the reader + */ + private Reader getReader(String view, String encoding) + { + // Get Input Stream + InputStream viewStream = getClass().getClassLoader().getResourceAsStream(view); + if (viewStream == null) + { + throw new ImporterException("Could not find view file " + view); + } + + // Wrap in buffered reader + try + { + InputStreamReader inputReader = (encoding == null) ? new InputStreamReader(viewStream) : new InputStreamReader(viewStream, encoding); + BufferedReader reader = new BufferedReader(inputReader); + return reader; + } + catch (UnsupportedEncodingException e) + { + throw new ImporterException("Could not create reader for view " + view + " as encoding " + encoding + " is not supported"); + } + } + + /** + * Bootstrap Binding + */ + private class BootstrapBinding implements ImporterBinding + { + private Properties configuration = null; + private ResourceBundle resourceBundle = null; + + /** + * Set Import Configuration + * + * @param configuration + */ + public void setConfiguration(Properties configuration) + { + this.configuration = configuration; + } + + /** + * Get Import Configuration + * + * @return configuration + */ + public Properties getConfiguration() + { + return this.configuration; + } + + /** + * Set Resource Bundle + * + * @param resourceBundle + */ + public void setResourceBundle(ResourceBundle resourceBundle) + { + this.resourceBundle = resourceBundle; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterBinding#getValue(java.lang.String) + */ + public String getValue(String key) + { + String value = null; + if (configuration != null) + { + value = configuration.getProperty(key); + } + if (value == null && resourceBundle != null) + { + value = resourceBundle.getString(key); + } + return value; + } + } + + /** + * Bootstrap Progress (debug logging) + */ + private class BootstrapProgress implements ImporterProgress + { + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Progress#nodeCreated(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName) + */ + public void nodeCreated(NodeRef nodeRef, NodeRef parentRef, QName assocName, QName childName) + { + if (logger.isDebugEnabled()) + logger.debug("Created node " + nodeRef + " (child name: " + childName + ") within parent " + parentRef + " (association type: " + assocName + ")"); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Progress#contentCreated(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public void contentCreated(NodeRef nodeRef, String sourceUrl) + { + if (logger.isDebugEnabled()) + logger.debug("Imported content from " + sourceUrl + " into node " + nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Progress#propertySet(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable) + */ + public void propertySet(NodeRef nodeRef, QName property, Serializable value) + { + if (logger.isDebugEnabled()) + logger.debug("Property " + property + " set to value " + value + " on node " + nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Progress#aspectAdded(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void aspectAdded(NodeRef nodeRef, QName aspect) + { + if (logger.isDebugEnabled()) + logger.debug("Added aspect " + aspect + " to node " + nodeRef); + } + + } + + /** + * Determine if bootstrap should take place + * + * @return true => yes, it should + */ + private boolean performBootstrap() + { + if (nodeService.exists(storeRef)) + { + return false; + } + + if (mustNotExistStoreUrls != null) + { + for (String storeUrl : mustNotExistStoreUrls) + { + StoreRef storeRef = new StoreRef(storeUrl); + if (nodeService.exists(storeRef)) + { + return false; + } + } + } + + return true; + } + +} diff --git a/source/java/org/alfresco/repo/importer/ImporterComponent.java b/source/java/org/alfresco/repo/importer/ImporterComponent.java new file mode 100644 index 0000000000..bd62d65861 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ImporterComponent.java @@ -0,0 +1,1008 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.XPathException; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.view.ImportPackageHandler; +import org.alfresco.service.cmr.view.ImporterBinding; +import org.alfresco.service.cmr.view.ImporterException; +import org.alfresco.service.cmr.view.ImporterProgress; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; +import org.xml.sax.ContentHandler; + + +/** + * Default implementation of the Importer Service + * + * @author David Caruana + */ +public class ImporterComponent + implements ImporterService +{ + // default importer + // TODO: Allow registration of plug-in parsers (by namespace) + private Parser viewParser; + + // supporting services + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + private BehaviourFilter behaviourFilter; + private NodeService nodeService; + private SearchService searchService; + private ContentService contentService; + private RuleService ruleService; + + // binding markers + private static final String START_BINDING_MARKER = "${"; + private static final String END_BINDING_MARKER = "}"; + + + /** + * @param viewParser the default parser + */ + public void setViewParser(Parser viewParser) + { + this.viewParser = viewParser; + } + + /** + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param searchService the service to perform path searches + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param behaviourFilter policy behaviour filter + */ + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + /** + * TODO: Remove this in favour of appropriate rule disabling + * + * @param ruleService rule service + */ + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterService#importView(java.io.InputStreamReader, org.alfresco.service.cmr.view.Location, java.util.Properties, org.alfresco.service.cmr.view.ImporterProgress) + */ + public void importView(Reader viewReader, Location location, ImporterBinding binding, ImporterProgress progress) + { + NodeRef nodeRef = getNodeRef(location, binding); + parserImport(nodeRef, location.getChildAssocType(), viewReader, new DefaultStreamHandler(), binding, progress); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterService#importView(org.alfresco.service.cmr.view.ImportPackageHandler, org.alfresco.service.cmr.view.Location, org.alfresco.service.cmr.view.ImporterBinding, org.alfresco.service.cmr.view.ImporterProgress) + */ + public void importView(ImportPackageHandler importHandler, Location location, ImporterBinding binding, ImporterProgress progress) throws ImporterException + { + importHandler.startImport(); + Reader dataFileReader = importHandler.getDataStream(); + NodeRef nodeRef = getNodeRef(location, binding); + parserImport(nodeRef, location.getChildAssocType(), dataFileReader, importHandler, binding, progress); + importHandler.endImport(); + } + + /** + * Get Node Reference from Location + * + * @param location the location to extract node reference from + * @param binding import configuration + * @return node reference + */ + private NodeRef getNodeRef(Location location, ImporterBinding binding) + { + ParameterCheck.mandatory("Location", location); + + // Establish node to import within + NodeRef nodeRef = location.getNodeRef(); + if (nodeRef == null) + { + // If a specific node has not been provided, default to the root + nodeRef = nodeService.getRootNode(location.getStoreRef()); + } + + // Resolve to path within node, if one specified + String path = location.getPath(); + if (path != null && path.length() >0) + { + // Create a valid path and search + path = bindPlaceHolder(path, binding); + path = createValidPath(path); + List nodeRefs = searchService.selectNodes(nodeRef, path, null, namespaceService, false); + if (nodeRefs.size() == 0) + { + throw new ImporterException("Path " + path + " within node " + nodeRef + " does not exist - the path must resolve to a valid location"); + } + if (nodeRefs.size() > 1) + { + throw new ImporterException("Path " + path + " within node " + nodeRef + " found too many locations - the path must resolve to one location"); + } + nodeRef = nodeRefs.get(0); + } + + // TODO: Check Node actually exists + + return nodeRef; + } + + /** + * Bind the specified value to the passed configuration values if it is a place holder + * + * @param value the value to bind + * @param binding the configuration properties to bind to + * @return the bound value + */ + private String bindPlaceHolder(String value, ImporterBinding binding) + { + if (binding != null) + { + int iStartBinding = value.indexOf(START_BINDING_MARKER); + while (iStartBinding != -1) + { + int iEndBinding = value.indexOf(END_BINDING_MARKER, iStartBinding + START_BINDING_MARKER.length()); + if (iEndBinding == -1) + { + throw new ImporterException("Cannot find end marker " + END_BINDING_MARKER + " within value " + value); + } + + String key = value.substring(iStartBinding + START_BINDING_MARKER.length(), iEndBinding); + String keyValue = binding.getValue(key); + value = StringUtils.replace(value, START_BINDING_MARKER + key + END_BINDING_MARKER, keyValue == null ? "" : keyValue); + iStartBinding = value.indexOf(START_BINDING_MARKER); + } + } + return value; + } + + /** + * Create a valid path + * + * @param path + * @return + */ + private String createValidPath(String path) + { + StringBuffer validPath = new StringBuffer(path.length()); + String[] segments = StringUtils.delimitedListToStringArray(path, "/"); + for (int i = 0; i < segments.length; i++) + { + if (segments[i] != null && segments[i].length() > 0) + { + String[] qnameComponents = QName.splitPrefixedQName(segments[i]); + QName segmentQName = QName.createQName(qnameComponents[0], QName.createValidLocalName(qnameComponents[1]), namespaceService); + validPath.append(segmentQName.toPrefixString()); + } + if (i < (segments.length -1)) + { + validPath.append("/"); + } + } + return validPath.toString(); + } + + /** + * Perform the actual import + * + * @param nodeRef node reference to import under + * @param childAssocType the child association type to import under + * @param inputStream the input stream to import from + * @param streamHandler the content property import stream handler + * @param binding import configuration + * @param progress import progress + */ + public void parserImport(NodeRef nodeRef, QName childAssocType, Reader viewReader, ImportPackageHandler streamHandler, ImporterBinding binding, ImporterProgress progress) + { + ParameterCheck.mandatory("Node Reference", nodeRef); + ParameterCheck.mandatory("View Reader", viewReader); + ParameterCheck.mandatory("Stream Handler", streamHandler); + + Importer nodeImporter = new NodeImporter(nodeRef, childAssocType, binding, streamHandler, progress); + try + { + nodeImporter.start(); + viewParser.parse(viewReader, nodeImporter); + nodeImporter.end(); + } + finally + { + nodeImporter.error(null); + } + } + + + public ContentHandler handlerImport(NodeRef nodeRef, QName childAssocType, ImportContentHandler handler, ImporterBinding binding, ImporterProgress progress) + { + ParameterCheck.mandatory("Node Reference", nodeRef); + + DefaultContentHandler defaultHandler = new DefaultContentHandler(handler); + ImportPackageHandler streamHandler = new ContentHandlerStreamHandler(defaultHandler); + Importer nodeImporter = new NodeImporter(nodeRef, childAssocType, binding, streamHandler, progress); + defaultHandler.setImporter(nodeImporter); + return defaultHandler; + } + + + + /** + * Default Importer strategy + * + * @author David Caruana + */ + private class NodeImporter + implements Importer + { + private NodeRef rootRef; + private QName rootAssocType; + private ImporterBinding binding; + private ImporterProgress progress; + private ImportPackageHandler streamHandler; + private List nodeRefs = new ArrayList(); + + // Flush threshold + private int flushThreshold = 500; + private int flushCount = 0; + + + /** + * Construct + * + * @param rootRef + * @param rootAssocType + * @param binding + * @param progress + */ + private NodeImporter(NodeRef rootRef, QName rootAssocType, ImporterBinding binding, ImportPackageHandler streamHandler, ImporterProgress progress) + { + this.rootRef = rootRef; + this.rootAssocType = rootAssocType; + this.binding = binding; + this.progress = progress; + this.streamHandler = streamHandler; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#getRootRef() + */ + public NodeRef getRootRef() + { + return rootRef; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#getRootAssocType() + */ + public QName getRootAssocType() + { + return rootAssocType; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#start() + */ + public void start() + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#importMetaData(java.util.Map) + */ + public void importMetaData(Map properties) + { + // Determine if we're importing a complete repository + String path = properties.get(QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "exportOf")); + if (path != null && path.equals("/")) + { + // Only allow complete repository import into root + NodeRef storeRootRef = nodeService.getRootNode(rootRef.getStoreRef()); + if (!storeRootRef.equals(rootRef)) + { + throw new ImporterException("A complete repository package cannot be imported here"); + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#importNode(org.alfresco.repo.importer.ImportNode) + */ + public NodeRef importNode(ImportNode context) + { + TypeDefinition nodeType = context.getTypeDefinition(); + NodeRef parentRef = context.getParentContext().getParentRef(); + QName assocType = getAssocType(context); + QName childQName = null; + + // Determine child name + String childName = context.getChildName(); + if (childName != null) + { + childName = bindPlaceHolder(childName, binding); + String[] qnameComponents = QName.splitPrefixedQName(childName); + childQName = QName.createQName(qnameComponents[0], QName.createValidLocalName(qnameComponents[1]), namespaceService); + } + else + { + Map typeProperties = context.getProperties(); + String name = (String)typeProperties.get(ContentModel.PROP_NAME); + if (name == null || name.length() == 0) + { + throw new ImporterException("Cannot import node of type " + nodeType.getName() + " - it does not have a name"); + } + + name = bindPlaceHolder(name, binding); + String localName = QName.createValidLocalName(name); + childQName = QName.createQName(assocType.getNamespaceURI(), localName); + } + + // Build initial map of properties + Map initialProperties = bindProperties(context); + + // Create initial node (but, first disable behaviour for the node to be created) + Set disabledBehaviours = getDisabledBehaviours(context); + for (QName disabledBehaviour: disabledBehaviours) + { + boolean alreadyDisabled = behaviourFilter.disableBehaviour(disabledBehaviour); + if (alreadyDisabled) + { + disabledBehaviours.remove(disabledBehaviour); + } + } + ChildAssociationRef assocRef = nodeService.createNode(parentRef, assocType, childQName, nodeType.getName(), initialProperties); + for (QName disabledBehaviour : disabledBehaviours) + { + behaviourFilter.enableBehaviour(disabledBehaviour); + } + + // Report creation + NodeRef nodeRef = assocRef.getChildRef(); + reportNodeCreated(assocRef); + reportPropertySet(nodeRef, initialProperties); + + // Disable behaviour for the node until the complete node (and its children have been imported) + for (QName disabledBehaviour : disabledBehaviours) + { + behaviourFilter.disableBehaviour(nodeRef, disabledBehaviour); + } + // TODO: Replace this with appropriate rule/action import handling + ruleService.disableRules(nodeRef); + + // Apply aspects + for (QName aspect : context.getNodeAspects()) + { + if (nodeService.hasAspect(nodeRef, aspect) == false) + { + nodeService.addAspect(nodeRef, aspect, null); // all properties previously added + reportAspectAdded(nodeRef, aspect); + } + } + + // import content, if applicable + for (Map.Entry property : context.getProperties().entrySet()) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(property.getKey()); + if (propertyDef != null && propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + importContent(nodeRef, property.getKey(), (String)property.getValue()); + } + } + + // Do we need to flush? + flushCount++; + if (flushCount > flushThreshold) + { + AlfrescoTransactionSupport.flush(); + flushCount = 0; + } + + return nodeRef; + } + + /** + * Import Node Content. + *

    + * The content URL, if present, will be a local URL. This import copies the content + * from the local URL to a server-assigned location. + * + * @param nodeRef containing node + * @param propertyName the name of the content-type property + * @param contentData the identifier of the content to import + */ + private void importContent(NodeRef nodeRef, QName propertyName, String importContentData) + { + // bind import content data description + DataTypeDefinition dataTypeDef = dictionaryService.getDataType(DataTypeDefinition.CONTENT); + importContentData = bindPlaceHolder(importContentData, binding); + ContentData contentData = (ContentData)DefaultTypeConverter.INSTANCE.convert(dataTypeDef, importContentData); + + String contentUrl = contentData.getContentUrl(); + if (contentUrl != null && contentUrl.length() > 0) + { + // import the content from the url + InputStream contentStream = streamHandler.importStream(contentUrl); + ContentWriter writer = contentService.getWriter(nodeRef, propertyName, true); + writer.setEncoding(contentData.getEncoding()); + writer.setMimetype(contentData.getMimetype()); + writer.putContent(contentStream); + reportContentCreated(nodeRef, contentUrl); + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#childrenImported(org.alfresco.service.cmr.repository.NodeRef) + */ + public void childrenImported(NodeRef nodeRef) + { + behaviourFilter.enableBehaviours(nodeRef); + ruleService.enableRules(nodeRef); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#end() + */ + public void end() + { + // Bind all node references to destination space + for (ImportedNodeRef importedRef : nodeRefs) + { + // Resolve path to node reference + NodeRef nodeRef = null; + + if (importedRef.value.startsWith("/")) + { + // resolve absolute path + SearchParameters searchParameters = new SearchParameters(); + searchParameters.addStore(importedRef.context.getNodeRef().getStoreRef()); + searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); + searchParameters.setQuery("PATH:\"" + importedRef.value + "\""); + searchParameters.excludeDataInTheCurrentTransaction(true); + ResultSet resultSet = searchService.query(searchParameters); + try + { + if (resultSet.length() > 0) + { + nodeRef = resultSet.getNodeRef(0); + } + } + finally + { + resultSet.close(); + } + } + else + { + // resolve relative path + try + { + List nodeRefs = searchService.selectNodes(importedRef.context.getNodeRef(), importedRef.value, null, namespaceService, false); + if (nodeRefs.size() > 0) + { + nodeRef = nodeRefs.get(0); + } + } + catch(XPathException e) + { + // attempt to resolve as a node reference + try + { + NodeRef directRef = new NodeRef(importedRef.value); + if (nodeService.exists(directRef)) + { + nodeRef = directRef; + } + } + catch(AlfrescoRuntimeException e1) + { + // Note: Invalid reference format + } + } + } + + // check that reference could be bound + if (nodeRef == null) + { + // TODO: Probably need an alternative mechanism here e.g. report warning + throw new ImporterException("Failed to find item referenced as " + importedRef.value); + } + + // Set node reference on source node + Set disabledBehaviours = getDisabledBehaviours(importedRef.context); + try + { + for (QName disabledBehaviour: disabledBehaviours) + { + boolean alreadyDisabled = behaviourFilter.disableBehaviour(importedRef.context.getNodeRef(), disabledBehaviour); + if (alreadyDisabled) + { + disabledBehaviours.remove(disabledBehaviour); + } + } + nodeService.setProperty(importedRef.context.getNodeRef(), importedRef.property, nodeRef); + } + finally + { + behaviourFilter.enableBehaviours(importedRef.context.getNodeRef()); + } + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.importer.Importer#error(java.lang.Throwable) + */ + public void error(Throwable e) + { + behaviourFilter.enableAllBehaviours(); + } + + /** + * Get appropriate child association type for node to import under + * + * @param context node to import + * @return child association type name + */ + private QName getAssocType(ImportNode context) + { + QName assocType = context.getParentContext().getAssocType(); + if (assocType != null) + { + // return explicitly set association type + return assocType; + } + + // + // Derive association type + // + + // build type and aspect list for node + List nodeTypes = new ArrayList(); + nodeTypes.add(context.getTypeDefinition().getName()); + for (QName aspect : context.getNodeAspects()) + { + nodeTypes.add(aspect); + } + + // build target class types for parent + Map targetTypes = new HashMap(); + QName parentType = nodeService.getType(context.getParentContext().getParentRef()); + ClassDefinition classDef = dictionaryService.getClass(parentType); + Map childAssocDefs = classDef.getChildAssociations(); + for (ChildAssociationDefinition childAssocDef : childAssocDefs.values()) + { + targetTypes.put(childAssocDef.getTargetClass().getName(), childAssocDef.getName()); + } + Set parentAspects = nodeService.getAspects(context.getParentContext().getParentRef()); + for (QName parentAspect : parentAspects) + { + classDef = dictionaryService.getClass(parentAspect); + childAssocDefs = classDef.getChildAssociations(); + for (ChildAssociationDefinition childAssocDef : childAssocDefs.values()) + { + targetTypes.put(childAssocDef.getTargetClass().getName(), childAssocDef.getName()); + } + } + + // find target class that is closest to node type or aspects + QName closestAssocType = null; + int closestHit = 1; + for (QName nodeType : nodeTypes) + { + for (QName targetType : targetTypes.keySet()) + { + QName testType = nodeType; + int howClose = 1; + while (testType != null) + { + howClose--; + if (targetType.equals(testType) && howClose < closestHit) + { + closestAssocType = targetTypes.get(targetType); + closestHit = howClose; + break; + } + ClassDefinition testTypeDef = dictionaryService.getClass(testType); + testType = (testTypeDef == null) ? null : testTypeDef.getParentName(); + } + } + } + + return closestAssocType; + } + + /** + * For the given import node, return the behaviours to disable during import + * + * @param context import node + * @return the disabled behaviours + */ + private Set getDisabledBehaviours(ImportNode context) + { + Set classNames = new HashSet(); + + // disable the type + TypeDefinition typeDef = context.getTypeDefinition(); + classNames.add(typeDef.getName()); + + // disable the aspects imported on the node + classNames.addAll(context.getNodeAspects()); + + // note: do not disable default aspects that are not imported on the node. + // this means they'll be added on import + + return classNames; + } + + /** + * Bind properties + * + * @param properties + * @return + */ + private Map bindProperties(ImportNode context) + { + Map properties = context.getProperties(); + Map datatypes = context.getPropertyDatatypes(); + Map boundProperties = new HashMap(properties.size()); + for (QName property : properties.keySet()) + { + // get property value + Serializable value = properties.get(property); + + // get property datatype + DataTypeDefinition valueDataType = datatypes.get(property); + if (valueDataType == null) + { + PropertyDefinition propDef = dictionaryService.getProperty(property); + if (propDef != null) + { + valueDataType = propDef.getDataType(); + } + } + + // filter out content properties (they're imported later) + if (valueDataType != null && valueDataType.getName().equals(DataTypeDefinition.CONTENT)) + { + continue; + } + + // bind property value to configuration and convert to appropriate type + if (value instanceof Collection) + { + List boundCollection = new ArrayList(); + for (String collectionValue : (Collection)value) + { + Serializable objValue = bindValue(context, property, valueDataType, collectionValue); + boundCollection.add(objValue); + } + value = (Serializable)boundCollection; + } + else + { + value = bindValue(context, property, valueDataType, (String)value); + } + boundProperties.put(property, value); + } + + return boundProperties; + } + + /** + * Bind property value + * + * @param valueType value type + * @param value string form of value + * @return the bound value + */ + private Serializable bindValue(ImportNode context, QName property, DataTypeDefinition valueType, String value) + { + Serializable objValue = null; + if (value != null && valueType != null) + { + String strValue = bindPlaceHolder(value, binding); + if (valueType.getName().equals(DataTypeDefinition.NODE_REF) || valueType.getName().equals(DataTypeDefinition.CATEGORY)) + { + if (value.length() > 0) + { + // record node reference for end-of-import binding + ImportedNodeRef importedRef = new ImportedNodeRef(context, property, strValue); + nodeRefs.add(importedRef); + objValue = new NodeRef(rootRef.getStoreRef(), "unresolved reference"); + } + } + else + { + objValue = (Serializable)DefaultTypeConverter.INSTANCE.convert(valueType, strValue); + } + } + return objValue; + } + + /** + * Helper to report node created progress + * + * @param progress + * @param childAssocRef + */ + private void reportNodeCreated(ChildAssociationRef childAssocRef) + { + if (progress != null) + { + progress.nodeCreated(childAssocRef.getChildRef(), childAssocRef.getParentRef(), childAssocRef.getTypeQName(), childAssocRef.getQName()); + } + } + + /** + * Helper to report content created progress + * + * @param progress + * @param nodeRef + * @param sourceUrl + */ + private void reportContentCreated(NodeRef nodeRef, String sourceUrl) + { + if (progress != null) + { + progress.contentCreated(nodeRef, sourceUrl); + } + } + + /** + * Helper to report aspect added progress + * + * @param progress + * @param nodeRef + * @param aspect + */ + private void reportAspectAdded(NodeRef nodeRef, QName aspect) + { + if (progress != null) + { + progress.aspectAdded(nodeRef, aspect); + } + } + + /** + * Helper to report property set progress + * + * @param progress + * @param nodeRef + * @param properties + */ + private void reportPropertySet(NodeRef nodeRef, Map properties) + { + if (progress != null) + { + for (QName property : properties.keySet()) + { + progress.propertySet(nodeRef, property, properties.get(property)); + } + } + } + } + + /** + * Imported Node Reference + * + * @author David Caruana + */ + private static class ImportedNodeRef + { + /** + * Construct + * + * @param context + * @param property + * @param value + */ + private ImportedNodeRef(ImportNode context, QName property, String value) + { + this.context = context; + this.property = property; + this.value = value; + } + + private ImportNode context; + private QName property; + private String value; + } + + /** + * Default Import Stream Handler + * + * @author David Caruana + */ + private static class DefaultStreamHandler + implements ImportPackageHandler + { + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#startImport() + */ + public void startImport() + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportStreamHandler#importStream(java.lang.String) + */ + public InputStream importStream(String content) + { + ResourceLoader loader = new DefaultResourceLoader(); + Resource resource = loader.getResource(content); + if (resource.exists() == false) + { + throw new ImporterException("Content URL " + content + " does not exist."); + } + + try + { + return resource.getInputStream(); + } + catch(IOException e) + { + throw new ImporterException("Failed to retrieve input stream for content URL " + content); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#getDataStream() + */ + public Reader getDataStream() + { + return null; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#endImport() + */ + public void endImport() + { + } + } + + /** + * Default Import Stream Handler + * + * @author David Caruana + */ + private static class ContentHandlerStreamHandler + implements ImportPackageHandler + { + private ImportContentHandler handler; + + /** + * Construct + * + * @param handler + */ + private ContentHandlerStreamHandler(ImportContentHandler handler) + { + this.handler = handler; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#startImport() + */ + public void startImport() + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportStreamHandler#importStream(java.lang.String) + */ + public InputStream importStream(String content) + { + return handler.importStream(content); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#getDataStream() + */ + public Reader getDataStream() + { + return null; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImportPackageHandler#endImport() + */ + public void endImport() + { + } + } + +} diff --git a/source/java/org/alfresco/repo/importer/ImporterComponentTest.java b/source/java/org/alfresco/repo/importer/ImporterComponentTest.java new file mode 100644 index 0000000000..0091ac0a2f --- /dev/null +++ b/source/java/org/alfresco/repo/importer/ImporterComponentTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ImporterProgress; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.debug.NodeStoreInspector; + + +public class ImporterComponentTest extends BaseSpringTest +{ + private ImporterService importerService; + private ImporterBootstrap importerBootstrap; + private NodeService nodeService; + private StoreRef storeRef; + private AuthenticationComponent authenticationComponent; + + + @Override + protected void onSetUpInTransaction() throws Exception + { + nodeService = (NodeService)applicationContext.getBean(ServiceRegistry.NODE_SERVICE.getLocalName()); + importerService = (ImporterService)applicationContext.getBean(ServiceRegistry.IMPORTER_SERVICE.getLocalName()); + + importerBootstrap = (ImporterBootstrap)applicationContext.getBean("importerBootstrap"); + + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + // Create the store + this.storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + } + + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + public void testImport() + throws Exception + { + InputStream test = getClass().getClassLoader().getResourceAsStream("org/alfresco/repo/importer/importercomponent_test.xml"); + InputStreamReader testReader = new InputStreamReader(test, "UTF-8"); + TestProgress testProgress = new TestProgress(); + Location location = new Location(storeRef); + importerService.importView(testReader, location, null, testProgress); + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + } + + + public void testBootstrap() + { + StoreRef bootstrapStoreRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + importerBootstrap.setStoreUrl(bootstrapStoreRef.toString()); + importerBootstrap.bootstrap(); + authenticationComponent.setSystemUserAsCurrentUser(); + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, bootstrapStoreRef)); + } + + + + private static class TestProgress implements ImporterProgress + { + public void nodeCreated(NodeRef nodeRef, NodeRef parentRef, QName assocName, QName childName) + { + System.out.println("TestProgress: created node " + nodeRef + " within parent " + parentRef + " named " + childName + + " (association " + assocName + ")"); + } + + public void contentCreated(NodeRef nodeRef, String sourceUrl) + { + System.out.println("TestProgress: created content " + nodeRef + " from url " + sourceUrl); + } + + public void propertySet(NodeRef nodeRef, QName property, Serializable value) + { + System.out.println("TestProgress: set property " + property + " on node " + nodeRef + " to value " + value); + } + + public void aspectAdded(NodeRef nodeRef, QName aspect) + { + System.out.println("TestProgress: added aspect " + aspect + " to node "); + } + } + + +} + diff --git a/source/java/org/alfresco/repo/importer/Parser.java b/source/java/org/alfresco/repo/importer/Parser.java new file mode 100644 index 0000000000..e374d25f26 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/Parser.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer; + +import java.io.Reader; + + +/** + * This interface represents the contract between the importer service and a + * parser (which is responsible for parsing the input stream and extracting + * node descriptions). + * + * The parser interacts with the passed importer to import nodes into the + * Repository. + * + * @author David Caruana + */ +public interface Parser +{ + /** + * Parse nodes from specified input stream and import via the provided importer + * + * @param viewReader + * @param importer + */ + public void parse(Reader viewReader, Importer importer); + +} diff --git a/source/java/org/alfresco/repo/importer/importercomponent_test.xml b/source/java/org/alfresco/repo/importer/importercomponent_test.xml new file mode 100644 index 0000000000..f56600a266 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/importercomponent_test.xml @@ -0,0 +1,74 @@ + + + + + + + unknown + 2005-10-19T19:18:01.387+01:00 + 1.0.0 (rc1b) + /system + + + + System + true + + + + + + + + `¬¦!£$%^&()-_=+tnu0000[]{};'#@~, + testuser + testuser + + dfsdfsdf + + + + + + + + ${username} + Fred + Bloggs + ../../cm:people_x0020_folder + + + + + + + + + + + Some Content + + ../cm:people_x0020_folder + ../cm:people_x0020_folder + + + + + + Translation of Some Content + + + + + Real content + contentUrl=classpath:org/alfresco/repo/importer/importercomponent_testfile.txt|mimetype=text|size=|encoding= + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/importer/importercomponent_testfile.txt b/source/java/org/alfresco/repo/importer/importercomponent_testfile.txt new file mode 100644 index 0000000000..3b4d665656 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/importercomponent_testfile.txt @@ -0,0 +1 @@ +A test import file \ No newline at end of file diff --git a/source/java/org/alfresco/repo/importer/view/ElementContext.java b/source/java/org/alfresco/repo/importer/view/ElementContext.java new file mode 100644 index 0000000000..7b9a8c84b1 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/view/ElementContext.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer.view; + +import org.alfresco.repo.importer.Importer; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.QName; + + +/** + * Maintains state about the currently imported element. + * + * @author David Caruana + * + */ +public class ElementContext +{ + // Dictionary Service + private DictionaryService dictionary; + + // Element Name + private QName elementName; + + // Importer + private Importer importer; + + + /** + * Construct + * + * @param dictionary + * @param elementName + * @param progress + */ + public ElementContext(QName elementName, DictionaryService dictionary, Importer importer) + { + this.elementName = elementName; + this.dictionary = dictionary; + this.importer = importer; + } + + /** + * @return the element name + */ + public QName getElementName() + { + return elementName; + } + + /** + * @return the dictionary service + */ + public DictionaryService getDictionaryService() + { + return dictionary; + } + + /** + * @return the importer + */ + public Importer getImporter() + { + return importer; + } +} diff --git a/source/java/org/alfresco/repo/importer/view/MetaDataContext.java b/source/java/org/alfresco/repo/importer/view/MetaDataContext.java new file mode 100644 index 0000000000..adebad55bc --- /dev/null +++ b/source/java/org/alfresco/repo/importer/view/MetaDataContext.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer.view; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.namespace.QName; + + +/** + * Represents View Meta Data + * + * @author David Caruana + */ +public class MetaDataContext extends ElementContext +{ + + private Map properties = new HashMap(); + + + /** + * Construct + * + * @param elementName + * @param dictionary + * @param importer + */ + public MetaDataContext(QName elementName, ElementContext context) + { + super(elementName, context.getDictionaryService(), context.getImporter()); + } + + + /** + * Set meta-data property + * + * @param property property name + * @param value property value + */ + public void setProperty(QName property, String value) + { + properties.put(property, value); + } + + + /** + * Get meta-data property + * + * @param property property name + * @return property value + */ + public String getProperty(QName property) + { + return properties.get(property); + } + + + /** + * Get all meta-data properties + * + * @return all meta-data properties + */ + public Map getProperties() + { + return properties; + } + +} diff --git a/source/java/org/alfresco/repo/importer/view/NodeContext.java b/source/java/org/alfresco/repo/importer/view/NodeContext.java new file mode 100644 index 0000000000..31bf3149c3 --- /dev/null +++ b/source/java/org/alfresco/repo/importer/view/NodeContext.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer.view; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.importer.ImportNode; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Maintains state about the currently imported node. + * + * @author David Caruana + * + */ +public class NodeContext extends ElementContext + implements ImportNode +{ + private ParentContext parentContext; + private NodeRef nodeRef; + private TypeDefinition typeDef; + private String childName; + private Map nodeAspects = new HashMap(); + private Map nodeChildAssocs = new HashMap(); + private Map nodeProperties = new HashMap(); + private Map propertyDatatypes = new HashMap(); + + + /** + * Construct + * + * @param elementName + * @param parentContext + * @param typeDef + */ + public NodeContext(QName elementName, ParentContext parentContext, TypeDefinition typeDef) + { + super(elementName, parentContext.getDictionaryService(), parentContext.getImporter()); + this.parentContext = parentContext; + this.typeDef = typeDef; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getParentContext() + */ + public ParentContext getParentContext() + { + return parentContext; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getTypeDefinition() + */ + public TypeDefinition getTypeDefinition() + { + return typeDef; + } + + /** + * Set Type Definition + * + * @param typeDef + */ + public void setTypeDefinition(TypeDefinition typeDef) + { + this.typeDef = typeDef; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getNodeRef() + */ + public NodeRef getNodeRef() + { + return nodeRef; + } + + /** + * @param nodeRef the node ref + */ + public void setNodeRef(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getChildName() + */ + public String getChildName() + { + return childName; + } + + /** + * @param childName the child name + */ + public void setChildName(String childName) + { + this.childName = childName; + } + + + /** + * Adds a collection property to the node + * + * @param property + */ + public void addPropertyCollection(QName property) + { + // Do not import properties of sys:referenceable or cm:versionable + // TODO: Make this configurable... + PropertyDefinition propDef = getDictionaryService().getProperty(property); + ClassDefinition classDef = (propDef == null) ? null : propDef.getContainerClass(); + if (classDef != null) + { + if (classDef.getName().equals(ContentModel.ASPECT_REFERENCEABLE) || + classDef.getName().equals(ContentModel.ASPECT_VERSIONABLE)) + { + return; + } + } + + // create collection and assign to property + Listvalues = new ArrayList(); + nodeProperties.put(property, (Serializable)values); + } + + + /** + * Adds a property to the node + * + * @param property the property name + * @param value the property value + */ + public void addProperty(QName property, String value) + { + // Do not import properties of sys:referenceable or cm:versionable + // TODO: Make this configurable... + PropertyDefinition propDef = getDictionaryService().getProperty(property); + ClassDefinition classDef = (propDef == null) ? null : propDef.getContainerClass(); + if (classDef != null) + { + if (classDef.getName().equals(ContentModel.ASPECT_REFERENCEABLE) || + classDef.getName().equals(ContentModel.ASPECT_VERSIONABLE)) + { + return; + } + } + + // Handle single / multi-valued cases + Serializable newValue = value; + Serializable existingValue = nodeProperties.get(property); + if (existingValue != null) + { + if (existingValue instanceof Collection) + { + // add to existing collection of values + ((Collection)existingValue).add(value); + newValue = existingValue; + } + else + { + // convert single to multi-valued + Listvalues = new ArrayList(); + values.add((String)existingValue); + values.add(value); + newValue = (Serializable)values; + } + } + nodeProperties.put(property, newValue); + } + + /** + * Adds a property datatype to the node + * + * @param property property name + * @param datatype property datatype + */ + public void addDatatype(QName property, DataTypeDefinition datatype) + { + propertyDatatypes.put(property, datatype); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getPropertyDatatypes() + */ + public Map getPropertyDatatypes() + { + return propertyDatatypes; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getProperties() + */ + public Map getProperties() + { + return nodeProperties; + } + + /** + * Adds an aspect to the node + * + * @param aspect the aspect + */ + public void addAspect(AspectDefinition aspect) + { + nodeAspects.put(aspect.getName(), aspect); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportNode#getNodeAspects() + */ + public Set getNodeAspects() + { + return nodeAspects.keySet(); + } + + /** + * Determine the type of definition (aspect, property, association) from the + * specified name + * + * @param defName + * @return the dictionary definition + */ + public Object determineDefinition(QName defName) + { + Object def = determineAspect(defName); + if (def == null) + { + def = determineProperty(defName); + if (def == null) + { + def = determineAssociation(defName); + } + } + return def; + } + + /** + * Determine if name referes to an aspect + * + * @param defName + * @return + */ + public AspectDefinition determineAspect(QName defName) + { + AspectDefinition def = null; + if (nodeAspects.containsKey(defName) == false) + { + def = getDictionaryService().getAspect(defName); + } + return def; + } + + /** + * Determine if name refers to a property + * + * @param defName + * @return + */ + public PropertyDefinition determineProperty(QName defName) + { + PropertyDefinition def = null; + if (nodeProperties.containsKey(defName) == false) + { + def = getDictionaryService().getProperty(typeDef.getName(), defName); + if (def == null) + { + Set allAspects = new HashSet(); + allAspects.addAll(typeDef.getDefaultAspects()); + allAspects.addAll(nodeAspects.values()); + for (AspectDefinition aspectDef : allAspects) + { + def = getDictionaryService().getProperty(aspectDef.getName(), defName); + if (def != null) + { + break; + } + } + } + } + return def; + } + + /** + * Determine if name referes to an association + * + * @param defName + * @return + */ + public AssociationDefinition determineAssociation(QName defName) + { + AssociationDefinition def = null; + if (nodeChildAssocs.containsKey(defName) == false) + { + def = getDictionaryService().getAssociation(defName); + } + return def; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return "NodeContext[childName=" + getChildName() + ",type=" + (typeDef == null ? "null" : typeDef.getName()) + ",nodeRef=" + nodeRef + + ",aspects=" + nodeAspects.values() + ",parentContext=" + parentContext.toString() + "]"; + } + +} diff --git a/source/java/org/alfresco/repo/importer/view/NodeItemContext.java b/source/java/org/alfresco/repo/importer/view/NodeItemContext.java new file mode 100644 index 0000000000..f43dcff6ff --- /dev/null +++ b/source/java/org/alfresco/repo/importer/view/NodeItemContext.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer.view; + +import org.alfresco.service.namespace.QName; + + +/** + * Represents Property Context + * + * @author David Caruana + * + */ +public class NodeItemContext extends ElementContext +{ + private NodeContext nodeContext; + + /** + * Construct + * + * @param elementName + * @param dictionary + * @param importer + */ + public NodeItemContext(QName elementName, NodeContext nodeContext) + { + super(elementName, nodeContext.getDictionaryService(), nodeContext.getImporter()); + this.nodeContext = nodeContext; + } + + /** + * Gets the Node Context + */ + public NodeContext getNodeContext() + { + return nodeContext; + } +} diff --git a/source/java/org/alfresco/repo/importer/view/ParentContext.java b/source/java/org/alfresco/repo/importer/view/ParentContext.java new file mode 100644 index 0000000000..36a778e36b --- /dev/null +++ b/source/java/org/alfresco/repo/importer/view/ParentContext.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer.view; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.importer.ImportParent; +import org.alfresco.repo.importer.Importer; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.ImporterException; +import org.alfresco.service.namespace.QName; + + +/** + * Maintains state about the parent context of the node being imported. + * + * @author David Caruana + * + */ +public class ParentContext extends ElementContext + implements ImportParent +{ + private NodeRef parentRef; + private QName assocType; + + + /** + * Construct + * + * @param dictionary + * @param configuration + * @param progress + * @param elementName + * @param parentRef + * @param assocType + */ + public ParentContext(QName elementName, DictionaryService dictionary, Importer importer) + { + super(elementName, dictionary, importer); + parentRef = importer.getRootRef(); + assocType = importer.getRootAssocType(); + } + + /** + * Construct (with unknown child association) + * + * @param elementName + * @param parent + */ + public ParentContext(QName elementName, NodeContext parent) + { + super(elementName, parent.getDictionaryService(), parent.getImporter()); + parentRef = parent.getNodeRef(); + } + + + /** + * Construct + * + * @param elementName + * @param parent + * @param childDef + */ + public ParentContext(QName elementName, NodeContext parent, ChildAssociationDefinition childDef) + { + this(elementName, parent); + + // Ensure association is valid for node + Set allAspects = new HashSet(); + for (AspectDefinition typeAspect : parent.getTypeDefinition().getDefaultAspects()) + { + allAspects.add(typeAspect.getName()); + } + allAspects.addAll(parent.getNodeAspects()); + TypeDefinition anonymousType = getDictionaryService().getAnonymousType(parent.getTypeDefinition().getName(), allAspects); + Map nodeAssociations = anonymousType.getChildAssociations(); + if (nodeAssociations.containsKey(childDef.getName()) == false) + { + throw new ImporterException("Association " + childDef.getName() + " is not valid for node " + parent.getTypeDefinition().getName()); + } + + parentRef = parent.getNodeRef(); + assocType = childDef.getName(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportParent#getParentRef() + */ + public NodeRef getParentRef() + { + return parentRef; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.ImportParent#getAssocType() + */ + public QName getAssocType() + { + return assocType; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return "ParentContext[parent=" + parentRef + ",assocType=" + getAssocType() + "]"; + } + +} diff --git a/source/java/org/alfresco/repo/importer/view/ViewParser.java b/source/java/org/alfresco/repo/importer/view/ViewParser.java new file mode 100644 index 0000000000..d9b1eaa64f --- /dev/null +++ b/source/java/org/alfresco/repo/importer/view/ViewParser.java @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.importer.view; + +import java.io.IOException; +import java.io.Reader; +import java.util.Stack; + +import org.alfresco.repo.importer.Importer; +import org.alfresco.repo.importer.Parser; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.ImporterException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + + +/** + * Importer for parsing and importing nodes given the Repository View schema. + * + * @author David Caruana + */ +public class ViewParser implements Parser +{ + // Logger + private static final Log logger = LogFactory.getLog(ViewParser.class); + + // View schema elements and attributes + private static final String VIEW_CHILD_NAME_ATTR = "childName"; + private static final String VIEW_DATATYPE_ATTR = "datatype"; + private static final String VIEW_ISNULL_ATTR = "isNull"; + private static final QName VIEW_METADATA = QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "metadata"); + private static final QName VIEW_VALUE_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "value"); + private static final QName VIEW_VALUES_QNAME = QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "values"); + private static final QName VIEW_ASPECTS = QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "aspects"); + private static final QName VIEW_PROPERTIES = QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "properties"); + private static final QName VIEW_ASSOCIATIONS = QName.createQName(NamespaceService.REPOSITORY_VIEW_1_0_URI, "associations"); + + // XML Pull Parser Factory + private XmlPullParserFactory factory; + + // Supporting services + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + + + /** + * Construct + */ + public ViewParser() + { + try + { + // Construct Xml Pull Parser Factory + factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), this.getClass()); + factory.setNamespaceAware(true); + } + catch (XmlPullParserException e) + { + throw new ImporterException("Failed to initialise view importer", e); + } + } + + /** + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.importer.Parser#parse(java.io.Reader, org.alfresco.repo.importer.Importer) + */ + public void parse(Reader viewReader, Importer importer) + { + try + { + XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(viewReader); + Stack contextStack = new Stack(); + + try + { + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.END_DOCUMENT; eventType = xpp.next()) + { + switch (eventType) + { + case XmlPullParser.START_TAG: + { + if (xpp.getDepth() == 1) + { + processRoot(xpp, importer, contextStack); + } + else + { + processStartElement(xpp, contextStack); + } + break; + } + case XmlPullParser.END_TAG: + { + processEndElement(xpp, contextStack); + break; + } + } + } + } + catch(Exception e) + { + throw new ImporterException("Failed to import package at line " + xpp.getLineNumber() + "; column " + xpp.getColumnNumber() + " due to error: " + e.getMessage(), e); + } + } + catch(XmlPullParserException e) + { + throw new ImporterException("Failed to parse view", e); + } + } + + /** + * Process start of xml element + * + * @param xpp + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processStartElement(XmlPullParser xpp, Stack contextStack) + throws XmlPullParserException, IOException + { + // Extract qualified name + QName defName = getName(xpp); + + // Process the element + Object context = contextStack.peek(); + + // Handle special view directives + if (defName.equals(VIEW_METADATA)) + { + contextStack.push(new MetaDataContext(defName, (ElementContext)context)); + } + else if (defName.equals(VIEW_ASPECTS) || defName.equals(VIEW_PROPERTIES) || defName.equals(VIEW_ASSOCIATIONS)) + { + if (context instanceof NodeItemContext) + { + throw new ImporterException("Cannot nest element " + defName + " within " + ((NodeItemContext)context).getElementName()); + } + if (!(context instanceof NodeContext)) + { + throw new ImporterException("Element " + defName + " can only be declared within a node"); + } + NodeContext nodeContext = (NodeContext)context; + contextStack.push(new NodeItemContext(defName, nodeContext)); + } + else + { + if (context instanceof MetaDataContext) + { + processMetaData(xpp, defName, contextStack); + } + else if (context instanceof ParentContext) + { + // Process type definition + TypeDefinition typeDef = dictionaryService.getType(defName); + if (typeDef == null) + { + throw new ImporterException("Type " + defName + " has not been defined in the Repository dictionary"); + } + processStartType(xpp, typeDef, contextStack); + return; + } + else if (context instanceof NodeContext) + { + // Process children of node + // Note: Process in the following order: aspects, properties and associations + Object def = ((NodeContext)context).determineDefinition(defName); + if (def == null) + { + throw new ImporterException("Definition " + defName + " is not valid; cannot find in Repository dictionary"); + } + + if (def instanceof AspectDefinition) + { + processAspect(xpp, (AspectDefinition)def, contextStack); + return; + } + else if (def instanceof PropertyDefinition) + { + processProperty(xpp, ((PropertyDefinition)def).getName(), contextStack); + return; + } + else if (def instanceof ChildAssociationDefinition) + { + processStartChildAssoc(xpp, (ChildAssociationDefinition)def, contextStack); + return; + } + else + { + // TODO: general association + } + } + else if (context instanceof NodeItemContext) + { + NodeItemContext nodeItemContext = (NodeItemContext)context; + NodeContext nodeContext = nodeItemContext.getNodeContext(); + QName itemName = nodeItemContext.getElementName(); + if (itemName.equals(VIEW_ASPECTS)) + { + AspectDefinition def = nodeContext.determineAspect(defName); + if (def == null) + { + throw new ImporterException("Aspect name " + defName + " is not valid; cannot find in Repository dictionary"); + } + processAspect(xpp, def, contextStack); + } + else if (itemName.equals(VIEW_PROPERTIES)) + { + // Note: Allow properties which do not have a data dictionary definition + processProperty(xpp, defName, contextStack); + } + else if (itemName.equals(VIEW_ASSOCIATIONS)) + { + // TODO: Handle general associations... + ChildAssociationDefinition def = (ChildAssociationDefinition)nodeContext.determineAssociation(defName); + if (def == null) + { + throw new ImporterException("Association name " + defName + " is not valid; cannot find in Repository dictionary"); + } + processStartChildAssoc(xpp, def, contextStack); + } + } + } + } + + /** + * Process Root + * + * @param xpp + * @param parentRef + * @param childAssocType + * @param configuration + * @param progress + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processRoot(XmlPullParser xpp, Importer importer, Stack contextStack) + throws XmlPullParserException, IOException + { + ParentContext parentContext = new ParentContext(getName(xpp), dictionaryService, importer); + contextStack.push(parentContext); + + if (logger.isDebugEnabled()) + logger.debug(indentLog("Pushed " + parentContext, contextStack.size() -1)); + } + + /** + * Process meta-data + * + * @param xpp + * @param metaDataName + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processMetaData(XmlPullParser xpp, QName metaDataName, Stack contextStack) + throws XmlPullParserException, IOException + { + MetaDataContext context = (MetaDataContext)contextStack.peek(); + + String value = null; + + int eventType = xpp.next(); + if (eventType == XmlPullParser.TEXT) + { + // Extract value + value = xpp.getText(); + eventType = xpp.next(); + } + if (eventType != XmlPullParser.END_TAG) + { + throw new ImporterException("Meta data element " + metaDataName + " is missing end tag"); + } + + context.setProperty(metaDataName, value); + } + + /** + * Process start of a node definition + * + * @param xpp + * @param typeDef + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processStartType(XmlPullParser xpp, TypeDefinition typeDef, Stack contextStack) + throws XmlPullParserException, IOException + { + ParentContext parentContext = (ParentContext)contextStack.peek(); + NodeContext context = new NodeContext(typeDef.getName(), parentContext, typeDef); + + // Extract child name if explicitly defined + String childName = xpp.getAttributeValue(NamespaceService.REPOSITORY_VIEW_1_0_URI, VIEW_CHILD_NAME_ATTR); + if (childName != null && childName.length() > 0) + { + context.setChildName(childName); + } + + contextStack.push(context); + + if (logger.isDebugEnabled()) + logger.debug(indentLog("Pushed " + context, contextStack.size() -1)); + } + + /** + * Process aspect definition + * + * @param xpp + * @param aspectDef + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processAspect(XmlPullParser xpp, AspectDefinition aspectDef, Stack contextStack) + throws XmlPullParserException, IOException + { + NodeContext context = peekNodeContext(contextStack); + context.addAspect(aspectDef); + + int eventType = xpp.next(); + if (eventType != XmlPullParser.END_TAG) + { + throw new ImporterException("Aspect " + aspectDef.getName() + " definition is not valid - it cannot contain any elements"); + } + + if (logger.isDebugEnabled()) + logger.debug(indentLog("Processed aspect " + aspectDef.getName(), contextStack.size())); + } + + /** + * Process property definition + * + * @param xpp + * @param propDef + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processProperty(XmlPullParser xpp, QName propertyName, Stack contextStack) + throws XmlPullParserException, IOException + { + NodeContext context = peekNodeContext(contextStack); + + // Extract single value + String value = ""; + int eventType = xpp.next(); + if (eventType == XmlPullParser.TEXT) + { + value = xpp.getText(); + eventType = xpp.next(); + } + if (eventType == XmlPullParser.END_TAG) + { + context.addProperty(propertyName, value); + } + else + { + + // Extract collection, if specified + boolean isCollection = false; + if (eventType == XmlPullParser.START_TAG) + { + QName name = getName(xpp); + if (name.equals(VIEW_VALUES_QNAME)) + { + context.addPropertyCollection(propertyName); + isCollection = true; + eventType = xpp.next(); + if (eventType == XmlPullParser.TEXT) + { + eventType = xpp.next(); + } + } + } + + // Extract decorated value + while (eventType == XmlPullParser.START_TAG) + { + QName name = getName(xpp); + if (!name.equals(VIEW_VALUE_QNAME)) + { + throw new ImporterException("Invalid view structure - expected element " + VIEW_VALUE_QNAME + " for property " + propertyName); + } + QName datatype = QName.createQName(xpp.getAttributeValue(NamespaceService.REPOSITORY_VIEW_1_0_URI, VIEW_DATATYPE_ATTR), namespaceService); + Boolean isNull = Boolean.valueOf(xpp.getAttributeValue(NamespaceService.REPOSITORY_VIEW_1_0_URI, VIEW_ISNULL_ATTR)); + String decoratedValue = isNull ? null : ""; + eventType = xpp.next(); + if (eventType == XmlPullParser.TEXT) + { + decoratedValue = xpp.getText(); + eventType = xpp.next(); + } + if (eventType == XmlPullParser.END_TAG) + { + context.addProperty(propertyName, decoratedValue); + if (datatype != null) + { + context.addDatatype(propertyName, dictionaryService.getDataType(datatype)); + } + } + else + { + throw new ImporterException("Value for property " + propertyName + " has not been defined correctly - missing end tag"); + } + eventType = xpp.next(); + if (eventType == XmlPullParser.TEXT) + { + eventType = xpp.next(); + } + } + + // End of value + if (eventType != XmlPullParser.END_TAG) + { + throw new ImporterException("Invalid view structure - property " + propertyName + " definition is invalid"); + } + + // End of collection + if (isCollection) + { + eventType = xpp.next(); + if (eventType == XmlPullParser.TEXT) + { + eventType = xpp.next(); + } + if (eventType != XmlPullParser.END_TAG) + { + throw new ImporterException("Invalid view structure - property " + propertyName + " definition is invalid"); + } + } + + } + + if (logger.isDebugEnabled()) + logger.debug(indentLog("Processed property " + propertyName, contextStack.size())); + } + + /** + * Process start of child association definition + * + * @param xpp + * @param childAssocDef + * @param contextStack + * @throws XmlPullParserException + * @throws IOException + */ + private void processStartChildAssoc(XmlPullParser xpp, ChildAssociationDefinition childAssocDef, Stack contextStack) + throws XmlPullParserException, IOException + { + NodeContext context = peekNodeContext(contextStack); + + if (context.getNodeRef() == null) + { + // Create Node + NodeRef nodeRef = context.getImporter().importNode(context); + context.setNodeRef(nodeRef); + } + + // Construct Child Association Context + ParentContext parentContext = new ParentContext(childAssocDef.getName(), context, childAssocDef); + contextStack.push(parentContext); + + if (logger.isDebugEnabled()) + logger.debug(indentLog("Pushed " + parentContext, contextStack.size() -1)); + } + + /** + * Process end of xml element + * + * @param xpp + * @param contextStack + */ + private void processEndElement(XmlPullParser xpp, Stack contextStack) + { + ElementContext context = contextStack.peek(); + if (context.getElementName().getLocalName().equals(xpp.getName()) && + context.getElementName().getNamespaceURI().equals(xpp.getNamespace())) + { + context = contextStack.pop(); + + if (logger.isDebugEnabled()) + logger.debug(indentLog("Popped " + context, contextStack.size())); + + if (context instanceof NodeContext) + { + processEndType((NodeContext)context); + } + else if (context instanceof ParentContext) + { + processEndChildAssoc((ParentContext)context); + } + else if (context instanceof MetaDataContext) + { + processEndMetaData((MetaDataContext)context); + } + } + } + + /** + * Process end of the type definition + * + * @param context + */ + private void processEndType(NodeContext context) + { + NodeRef nodeRef = context.getNodeRef(); + if (nodeRef == null) + { + nodeRef = context.getImporter().importNode(context); + context.setNodeRef(nodeRef); + } + context.getImporter().childrenImported(nodeRef); + } + + /** + * Process end of the child association + * + * @param context + */ + private void processEndChildAssoc(ParentContext context) + { + } + + /** + * Process end of meta data + * + * @param context + */ + private void processEndMetaData(MetaDataContext context) + { + context.getImporter().importMetaData(context.getProperties()); + } + + /** + * Get parent Node Context + * + * @param contextStack context stack + * @return node context + */ + private NodeContext peekNodeContext(Stack contextStack) + { + ElementContext context = contextStack.peek(); + if (context instanceof NodeContext) + { + return (NodeContext)context; + } + else if (context instanceof NodeItemContext) + { + return ((NodeItemContext)context).getNodeContext(); + } + throw new ImporterException("Internal error: Failed to retrieve node context"); + } + + /** + * Helper to create Qualified name from current xml element + * + * @param xpp + * @return + */ + private QName getName(XmlPullParser xpp) + { + // Ensure namespace is valid + String uri = xpp.getNamespace(); + if (namespaceService.getURIs().contains(uri) == false) + { + throw new ImporterException("Namespace URI " + uri + " has not been defined in the Repository dictionary"); + } + + // Construct name + String name = xpp.getName(); + return QName.createQName(uri, name); + } + + /** + * Helper to indent debug output + * + * @param msg + * @param depth + * @return + */ + private String indentLog(String msg, int depth) + { + StringBuffer buf = new StringBuffer(1024); + for (int i = 0; i < depth; i++) + { + buf.append(' '); + } + buf.append(msg); + return buf.toString(); + } + +} diff --git a/source/java/org/alfresco/repo/lock/LockBehaviourImplTest.java b/source/java/org/alfresco/repo/lock/LockBehaviourImplTest.java new file mode 100644 index 0000000000..79153b2df7 --- /dev/null +++ b/source/java/org/alfresco/repo/lock/LockBehaviourImplTest.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.lock; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.lock.NodeLockedException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.TestWithUserUtils; + +/** + * LockBehaviourImpl Unit Test. + * + * @author Roy Wetherall + */ +public class LockBehaviourImplTest extends BaseSpringTest +{ + /** + * The lock service + */ + private LockService lockService; + + /** + * The version service + */ + private VersionService versionService; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The authentication service + */ + private AuthenticationService authenticationService; + + private PermissionService permissionService; + + /** + * Node references used in the tests + */ + private NodeRef nodeRef; + private NodeRef noAspectNode; + + /** + * Store reference + */ + private StoreRef storeRef; + + /** + * User details + */ + private static final String PWD = "password"; + private static final String GOOD_USER_NAME = "goodUser"; + private static final String BAD_USER_NAME = "badUser"; + + NodeRef rootNodeRef; + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)applicationContext.getBean("dbNodeService"); + this.lockService = (LockService)applicationContext.getBean("lockService"); + this.versionService = (VersionService)applicationContext.getBean("versionService"); + this.authenticationService = (AuthenticationService)applicationContext.getBean("authenticationService"); + this.permissionService = (PermissionService)applicationContext.getBean("permissionService"); + authenticationService.clearCurrentSecurityContext(); + + // Create the node properties + HashMap nodeProperties = new HashMap(); + nodeProperties.put(QName.createQName("{test}property1"), "value1"); + + // Create a workspace that contains the 'live' nodes + this.storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + + // Get a reference to the root node + rootNodeRef = this.nodeService.getRootNode(this.storeRef); + + // Create node + this.nodeRef = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}ParentNode"), + ContentModel.TYPE_FOLDER, + nodeProperties).getChildRef(); + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, new HashMap()); + assertNotNull(this.nodeRef); + + // Create a node with no lockAspect + this.noAspectNode = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}noAspectNode"), + ContentModel.TYPE_CONTAINER, + nodeProperties).getChildRef(); + assertNotNull(this.noAspectNode); + + // Create the users + TestWithUserUtils.createUser(GOOD_USER_NAME, PWD, rootNodeRef, this.nodeService, this.authenticationService); + TestWithUserUtils.createUser(BAD_USER_NAME, PWD, rootNodeRef, this.nodeService, this.authenticationService); + + // Stash the user node ref's for later use + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + permissionService.setPermission(rootNodeRef, GOOD_USER_NAME.toLowerCase(), PermissionService.ALL_PERMISSIONS, true); + permissionService.setPermission(rootNodeRef, BAD_USER_NAME.toLowerCase(), PermissionService.READ, true); + } + + /** + * Test checkForLock (no user specified) + */ + public void testCheckForLockNoUser() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + this.lockService.checkForLock(this.nodeRef); + this.lockService.checkForLock(this.noAspectNode); + + // Give the node a write lock (as the good user) + this.lockService.lock(this.nodeRef, LockType.WRITE_LOCK); + this.lockService.checkForLock(this.nodeRef); + + // Give the node a read only lock (as the good user) + this.lockService.unlock(this.nodeRef); + this.lockService.lock(this.nodeRef, LockType.READ_ONLY_LOCK); + try + { + this.lockService.checkForLock(this.nodeRef); + fail("The node locked exception should have been raised"); + } + catch (NodeLockedException exception) + { + // Correct behaviour + } + + // Give the node a write lock (as the bad user) + this.lockService.unlock(this.nodeRef); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.lock(this.nodeRef, LockType.WRITE_LOCK); + try + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.checkForLock(this.nodeRef); + fail("The node locked exception should have been raised"); + } + catch (NodeLockedException exception) + { + // Correct behaviour + } + + // Give the node a read only lock (as the bad user) + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.unlock(this.nodeRef); + this.lockService.lock(this.nodeRef, LockType.READ_ONLY_LOCK); + try + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.checkForLock(this.nodeRef); + fail("The node locked exception should have been raised"); + } + catch (NodeLockedException exception) + { + // Correct behaviour + } + } + + public void testCheckForLockWhenExpired() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + this.lockService.lock(this.nodeRef, LockType.READ_ONLY_LOCK, 1); + try + { + this.lockService.checkForLock(this.nodeRef); + fail("Should be locked."); + } + catch (NodeLockedException e) + { + // Expected + } + + try {Thread.sleep(2*1000); } catch (Exception e) {}; + + // Should now have expired so the node should no longer appear to be locked + this.lockService.checkForLock(this.nodeRef); + } + + /** + * Test version service lock checking + */ + public void testVersionServiceLockBehaviour01() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Add the version aspect to the node + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + try + { + this.versionService.createVersion(this.nodeRef, new HashMap()); + } + catch (NodeLockedException exception) + { + fail("There is no lock so this should have worked."); + } + + // Lock the node as the good user with a write lock + this.lockService.lock(this.nodeRef, LockType.WRITE_LOCK); + try + { + this.versionService.createVersion(this.nodeRef, new HashMap()); + } + catch (NodeLockedException exception) + { + fail("Tried to version as the lock owner so should work."); + } + this.lockService.unlock(this.nodeRef); + + // Lock the node as the good user with a read only lock + this.lockService.lock(this.nodeRef, LockType.READ_ONLY_LOCK); + try + { + this.versionService.createVersion(this.nodeRef, new HashMap()); + fail("Should have failed since this node has been locked with a read only lock."); + } + catch (NodeLockedException exception) + { + } + this.lockService.unlock(this.nodeRef); + } + + /** + * Test version service lock checking + */ + public void testVersionServiceLockBehaviour02() + { + // Add the version aspect to the node + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + // Lock the node as the bad user with a write lock + this.lockService.lock(this.nodeRef, LockType.WRITE_LOCK); + try + { + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + this.versionService.createVersion(this.nodeRef, new HashMap()); + fail("Should have failed since this node has been locked by another user with a write lock."); + } + catch (NodeLockedException exception) + { + } + } + + /** + * Test that the node service lock behaviour is as we expect + * + */ + @SuppressWarnings("unused") + public void testNodeServiceLockBehaviour() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Check that we can create a new node and set of it properties when no lock is present + ChildAssociationRef childAssocRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName("{test}nodeServiceLockTest"), + ContentModel.TYPE_CONTAINER); + NodeRef nodeRef = childAssocRef.getChildRef(); + + // Lets lock the parent node and check that whether we can still create a new node + this.lockService.lock(this.nodeRef, LockType.WRITE_LOCK); + ChildAssociationRef childAssocRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName("{test}nodeServiceLockTest"), + ContentModel.TYPE_CONTAINER); + NodeRef nodeRef2 = childAssocRef.getChildRef(); + + // Lets check that we can do other stuff with the node since we have it locked + this.nodeService.setProperty(this.nodeRef, QName.createQName("{test}prop1"), "value1"); + Map propMap = new HashMap(); + propMap.put(QName.createQName("{test}prop2"), "value2"); + this.nodeService.setProperties(this.nodeRef, propMap); + this.nodeService.removeAspect(this.nodeRef, ContentModel.ASPECT_VERSIONABLE); + // TODO there are various other calls that could be more vigirously checked + + // Lock the node as the 'bad' user + this.lockService.unlock(this.nodeRef); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.lock(this.nodeRef, LockType.WRITE_LOCK); + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Lets check that we can't create a new child + try + { + this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName("{test}nodeServiceLockTest"), + ContentModel.TYPE_CONTAINER); + fail("The parent is locked so a new child should not have been created."); + } + catch(NodeLockedException exception) + { + } + + // TODO various other tests along these lines ... + + // TODO check that delete is also working + } + +} diff --git a/source/java/org/alfresco/repo/lock/LockServiceImpl.java b/source/java/org/alfresco/repo/lock/LockServiceImpl.java new file mode 100644 index 0000000000..2239a8d0b4 --- /dev/null +++ b/source/java/org/alfresco/repo/lock/LockServiceImpl.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.lock; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.lock.NodeLockedException; +import org.alfresco.service.cmr.lock.UnableToAquireLockException; +import org.alfresco.service.cmr.lock.UnableToReleaseLockException; +import org.alfresco.service.cmr.repository.AspectMissingException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Simple Lock service implementation + * + * @author Roy Wetherall + */ +public class LockServiceImpl implements LockService +{ + /** + * The node service + */ + private NodeService nodeService; + + /** + * The policy component + */ + private PolicyComponent policyComponent; + + /** + * List of node ref's to ignore when checking for locks + */ + private Set ignoreNodeRefs = new HashSet(); + + /** + * The authentication service + */ + private AuthenticationService authenticationService; + + /** + * The ownable service + * + */ + private OwnableService ownableService; + + /** + * The search service + */ + private SearchService searchService; + + /** + * Set the node service + * + * @param nodeService + * the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the policy component + * + * @param policyComponent + * the policy componentO + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Sets the authentication service + * + * @param authenticationService + * the authentication service + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * Sets the ownable service + * + * @param ownableService + * the ownable service + */ + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + /** + * Set the search service + * + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Initialise methods called by Spring framework + */ + public void initialise() + { + // Register the various class behaviours to enable lock checking + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "beforeCreateVersion"), ContentModel.ASPECT_LOCKABLE, + new JavaBehaviour(this, "checkForLock")); + this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "beforeUpdateNode"), + ContentModel.ASPECT_LOCKABLE, new JavaBehaviour(this, "checkForLock")); + this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), + ContentModel.ASPECT_LOCKABLE, new JavaBehaviour(this, "checkForLock")); + + // Register onCopy class behaviour + this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + ContentModel.ASPECT_LOCKABLE, new JavaBehaviour(this, "onCopy")); + + // Register the onCreateVersion behavior for the version aspect + this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateVersion"), + ContentModel.ASPECT_LOCKABLE, new JavaBehaviour(this, "onCreateVersion")); + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#lock(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.cmr.lock.LockType) + */ + public synchronized void lock(NodeRef nodeRef, LockType lockType) + { + // Lock with no expiration + lock(nodeRef, lockType, 0); + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#lock(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.cmr.lock.LockType, int) + */ + public synchronized void lock(NodeRef nodeRef, LockType lockType, int timeToExpire) + { + // Check for lock aspect + checkForLockApsect(nodeRef); + + // Get the current user name + String userName = getUserName(); + + // Set a default value + if (lockType == null) + { + lockType = LockType.WRITE_LOCK; + } + + LockStatus currentLockStatus = getLockStatus(nodeRef, userName); + if (LockStatus.LOCKED.equals(currentLockStatus) == true) + { + // Error since we are trying to lock a locked node + throw new UnableToAquireLockException(nodeRef); + } + else if (LockStatus.NO_LOCK.equals(currentLockStatus) == true || + LockStatus.LOCK_EXPIRED.equals(currentLockStatus) == true || + LockStatus.LOCK_OWNER.equals(currentLockStatus) == true) + { + this.ignoreNodeRefs.add(nodeRef); + try + { + // Set the current user as the lock owner + this.nodeService.setProperty(nodeRef, ContentModel.PROP_LOCK_OWNER, userName); + this.nodeService.setProperty(nodeRef, ContentModel.PROP_LOCK_TYPE, lockType.toString()); + setExpiryDate(nodeRef, timeToExpire); + } + finally + { + this.ignoreNodeRefs.remove(nodeRef); + } + } + } + + /** + * Helper method to set the expiry date based on the time to expire provided + * + * @param nodeRef the node reference + * @param timeToExpire the time to expire (in seconds) + */ + private void setExpiryDate(NodeRef nodeRef, int timeToExpire) + { + // Set the expiry date + Date expiryDate = null; + if (timeToExpire > 0) + { + expiryDate = new Date(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(expiryDate); + calendar.add(Calendar.SECOND, timeToExpire); + expiryDate = calendar.getTime(); + } + + this.nodeService.setProperty(nodeRef, ContentModel.PROP_EXPIRY_DATE, expiryDate); + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#lock(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.cmr.lock.LockType, int, boolean) + */ + public synchronized void lock(NodeRef nodeRef, LockType lockType, int timeToExpire, boolean lockChildren) + throws UnableToAquireLockException + { + lock(nodeRef, lockType, timeToExpire); + + if (lockChildren == true) + { + Collection childAssocRefs = this.nodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + lock(childAssocRef.getChildRef(), lockType, timeToExpire, lockChildren); + } + } + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#lock(java.util.Collection, java.lang.String, org.alfresco.service.cmr.lock.LockType, int) + */ + public synchronized void lock(Collection nodeRefs, LockType lockType, int timeToExpire) + throws UnableToAquireLockException + { + // Lock each of the specifed nodes + for (NodeRef nodeRef : nodeRefs) + { + lock(nodeRef, lockType, timeToExpire); + } + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#unlock(NodeRef, String) + */ + public synchronized void unlock(NodeRef nodeRef) throws UnableToReleaseLockException + { + // Check for lock aspect + checkForLockApsect(nodeRef); + + // Get the current user name + //String userName = getUserName(); + + //LockStatus lockStatus = getLockStatus(nodeRef, userName); + //if (LockStatus.LOCKED.equals(lockStatus) == true) + // { + // // Error since the lock can only be released by the lock owner + // throw new UnableToReleaseLockException(nodeRef); + // } + // else if (LockStatus.LOCK_OWNER.equals(lockStatus) == true) + // { + this.ignoreNodeRefs.add(nodeRef); + try + { + // Clear the lock owner + this.nodeService.setProperty(nodeRef, ContentModel.PROP_LOCK_OWNER, null); + this.nodeService.setProperty(nodeRef, ContentModel.PROP_LOCK_TYPE, null); + } finally + { + this.ignoreNodeRefs.remove(nodeRef); + } + //} + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#unlock(NodeRef, String, + * boolean) + */ + public synchronized void unlock(NodeRef nodeRef, boolean unlockChildren) + throws UnableToReleaseLockException + { + // Unlock the parent + unlock(nodeRef); + + if (unlockChildren == true) + { + // Get the children and unlock them + Collection childAssocRefs = this.nodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + unlock(childAssocRef.getChildRef(), unlockChildren); + } + } + } + + /** + * @see org.alfresco.repo.lock.LockService#unlock(Collection, + * String) + */ + public synchronized void unlock(Collection nodeRefs) throws UnableToReleaseLockException + { + for (NodeRef nodeRef : nodeRefs) + { + unlock(nodeRef); + } + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#getLockStatus(NodeRef) + */ + public LockStatus getLockStatus(NodeRef nodeRef) + { + return getLockStatus(nodeRef, getUserName()); + } + + /** + * Gets the lock statuc for a node and a user name + * + * @param nodeRef the node reference + * @param userName the user name + * @return the lock status + */ + private LockStatus getLockStatus(NodeRef nodeRef, String userName) + { + LockStatus result = LockStatus.NO_LOCK; + + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE) == true) + + { + // Get the current lock owner + String currentUserRef = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_LOCK_OWNER); + String owner = ownableService.getOwner(nodeRef); + if (currentUserRef != null) + { + Date expiryDate = (Date)this.nodeService.getProperty(nodeRef, ContentModel.PROP_EXPIRY_DATE); + if (expiryDate != null && expiryDate.before(new Date()) == true) + { + // Indicate that the lock has expired + result = LockStatus.LOCK_EXPIRED; + } + else + { + if (currentUserRef.equals(userName) == true) + { + result = LockStatus.LOCK_OWNER; + } + else if ((owner != null) && owner.equals(userName)) + { + result = LockStatus.LOCK_OWNER; + } + else + { + result = LockStatus.LOCKED; + } + } + } + + } + return result; + + } + + /** + * @see LockService#getLockType(NodeRef) + */ + public LockType getLockType(NodeRef nodeRef) + { + LockType result = null; + + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE) == true) + { + String lockTypeString = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_LOCK_TYPE); + if (lockTypeString != null) + { + result = LockType.valueOf(lockTypeString); + } + } + + return result; + } + + /** + * Checks for the lock aspect. Adds if missing. + * + * @param nodeRef + * the node reference + */ + private void checkForLockApsect(NodeRef nodeRef) + { + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE) == false) + { + this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_LOCKABLE, null); + } + } + + /** + * @see LockService#checkForLock(NodeRef) + */ + public void checkForLock(NodeRef nodeRef) throws NodeLockedException + { + String userName = getUserName(); + + // Ensure we have found a node reference + if (nodeRef != null && userName != null) + { + // Check to see if should just ignore this node + if (this.ignoreNodeRefs.contains(nodeRef) == false) + { + try + { + // Get the current lock status on the node ref + LockStatus currentLockStatus = getLockStatus(nodeRef, userName); + + LockType lockType = getLockType(nodeRef); + if (LockType.WRITE_LOCK.equals(lockType) == true && + LockStatus.LOCKED.equals(currentLockStatus) == true) + { + // Error since we are trying to preform an operation + // on a locked node + throw new NodeLockedException(nodeRef); + } + else if (LockType.READ_ONLY_LOCK.equals(lockType) == true && + (LockStatus.LOCKED.equals(currentLockStatus) == true || LockStatus.LOCK_OWNER.equals(currentLockStatus) == true)) + { + // Error since there is a read only lock on this object + // and all + // modifications are prevented + throw new NodeLockedException(nodeRef); + } + } + catch (AspectMissingException exception) + { + // Ignore since this indicates that the node does not have + // the lock + // aspect applied + } + } + } + } + + /** + * OnCopy behaviour implementation for the lock aspect. + *

    + * Ensures that the propety values of the lock aspect are not copied onto + * the destination node. + * + * @see org.alfresco.repo.copy.CopyServicePolicies.OnCopyNodePolicy#onCopyNode(QName, + * NodeRef, StoreRef, boolean, PolicyScope) + */ + public void onCopy(QName sourceClassRef, NodeRef sourceNodeRef, StoreRef destinationStoreRef, + boolean copyToNewNode, PolicyScope copyDetails) + { + // Add the lock aspect, but do not copy any of the properties + copyDetails.addAspect(ContentModel.ASPECT_LOCKABLE); + } + + /** + * OnCreateVersion behaviour for the lock aspect + *

    + * Ensures that the property valies of the lock aspect are not 'frozen' in + * the version store. + * + * @param classRef + * the class reference + * @param versionableNode + * the versionable node reference + * @param versionProperties + * the version properties + * @param nodeDetails + * the details of the node to be versioned + */ + public void onCreateVersion(QName classRef, NodeRef versionableNode, Map versionProperties, + PolicyScope nodeDetails) + { + // Add the lock aspect, but do not version the property values + nodeDetails.addAspect(ContentModel.ASPECT_LOCKABLE); + } + + /** + * Get the current user reference + * + * @return the current user reference + */ + private String getUserName() + { + return this.authenticationService.getCurrentUserName(); + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#getLocks() + */ + public List getLocks(StoreRef storeRef) + { + return getLocks( + storeRef, + "ASPECT:\"" + ContentModel.ASPECT_LOCKABLE.toString() + + "\" +@\\{http\\://www.alfresco.org/model/content/1.0\\}" + ContentModel.PROP_LOCK_OWNER.getLocalName() + ":\"" + getUserName() + "\""); + } + + /** + * Get the locks given a store and query string. + * + * @param storeRef the store reference + * @param query the query string + * @return the locked nodes + */ + private List getLocks(StoreRef storeRef, String query) + { + List result = new ArrayList(); + ResultSet resultSet = null; + try + { + resultSet = this.searchService.query( + storeRef, + SearchService.LANGUAGE_LUCENE, + query); + result = resultSet.getNodeRefs(); + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + return result; + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#getLocks(org.alfresco.service.cmr.lock.LockType) + */ + public List getLocks(StoreRef storeRef, LockType lockType) + { + return getLocks( + storeRef, + "ASPECT:\"" + ContentModel.ASPECT_LOCKABLE.toString() + + "\" +@\\{http\\://www.alfresco.org/model/content/1.0\\}" + ContentModel.PROP_LOCK_OWNER.getLocalName() + ":\"" + getUserName() + "\"" + + " +@\\{http\\://www.alfresco.org/model/content/1.0\\}" + ContentModel.PROP_LOCK_TYPE.getLocalName() + ":\"" + lockType.toString() + "\""); + } +} diff --git a/source/java/org/alfresco/repo/lock/LockServiceImplTest.java b/source/java/org/alfresco/repo/lock/LockServiceImplTest.java new file mode 100644 index 0000000000..4ca40fbb27 --- /dev/null +++ b/source/java/org/alfresco/repo/lock/LockServiceImplTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.lock; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.lock.UnableToAquireLockException; +import org.alfresco.service.cmr.lock.UnableToReleaseLockException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.TestWithUserUtils; + +/** + * Simple lock service test + * + * @author Roy Wetherall + */ +public class LockServiceImplTest extends BaseSpringTest +{ + /** + * Services used in tests + */ + private NodeService nodeService; + private LockService lockService; + private AuthenticationService authenticationService; + + /** + * Data used in tests + */ + private NodeRef parentNode; + private NodeRef childNode1; + private NodeRef childNode2; + private NodeRef noAspectNode; + + private static final String GOOD_USER_NAME = "goodUser"; + private static final String BAD_USER_NAME = "badUser"; + private static final String PWD = "password"; + + NodeRef rootNodeRef; + private StoreRef storeRef; + + /** + * Called during the transaction setup + */ + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)applicationContext.getBean("dbNodeService"); + this.lockService = (LockService)applicationContext.getBean("lockService"); + this.authenticationService = (AuthenticationService)applicationContext.getBean("authenticationService"); + authenticationService.clearCurrentSecurityContext(); + + // Create the node properties + HashMap nodeProperties = new HashMap(); + nodeProperties.put(QName.createQName("{test}property1"), "value1"); + + // Create a workspace that contains the 'live' nodes + storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + + // Get a reference to the root node + rootNodeRef = this.nodeService.getRootNode(storeRef); + + // Create node + this.parentNode = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}ParentNode"), + ContentModel.TYPE_CONTAINER, + nodeProperties).getChildRef(); + this.nodeService.addAspect(this.parentNode, ContentModel.ASPECT_LOCKABLE, new HashMap()); + HashMap audProps = new HashMap(); + audProps.put(ContentModel.PROP_CREATOR, "Monkey"); + this.nodeService.addAspect(this.parentNode, ContentModel.ASPECT_AUDITABLE, audProps); + assertNotNull(this.parentNode); + + // Add some children to the node + this.childNode1 = this.nodeService.createNode( + this.parentNode, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}ChildNode1"), + ContentModel.TYPE_CONTAINER, + nodeProperties).getChildRef(); + this.nodeService.addAspect(this.childNode1, ContentModel.ASPECT_LOCKABLE, new HashMap()); + assertNotNull(this.childNode1); + this.childNode2 = this.nodeService.createNode( + this.parentNode, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}ChildNode2"), + ContentModel.TYPE_CONTAINER, + nodeProperties).getChildRef(); + this.nodeService.addAspect(this.childNode2, ContentModel.ASPECT_LOCKABLE, new HashMap()); + assertNotNull(this.childNode2); + + // Create a node with no lockAspect + this.noAspectNode = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}noAspectNode"), + ContentModel.TYPE_CONTAINER, + nodeProperties).getChildRef(); + assertNotNull(this.noAspectNode); + + // Create the users + TestWithUserUtils.createUser(GOOD_USER_NAME, PWD, rootNodeRef, this.nodeService, this.authenticationService); + TestWithUserUtils.createUser(BAD_USER_NAME, PWD, rootNodeRef, this.nodeService, this.authenticationService); + + // Stash the user node ref's for later use + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + } + + /** + * Test lock + */ + public void testLock() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Check that the node is not currently locked + assertEquals( + LockStatus.NO_LOCK, + this.lockService.getLockStatus(this.parentNode)); + + + // Test valid lock + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK); + assertEquals( + LockStatus.LOCK_OWNER, + this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + assertEquals( + LockStatus.LOCKED, + this.lockService.getLockStatus(this.parentNode)); + + // Test lock when already locked + try + { + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK); + fail("The user should not be able to lock the node since it is already locked by another user."); + } + catch (UnableToAquireLockException exception) + { + } + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Test already locked by this user + try + { + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK); + } + catch (Exception exception) + { + fail("No error should be thrown when a node is re-locked by the current lock owner."); + } + + // Test with no apect node + this.lockService.lock(this.noAspectNode, LockType.WRITE_LOCK); + } + + /** + * Test lock with lockChildren == true + */ + // TODO + public void testLockChildren() + { + } + + /** + * Test lock with collection + */ + // TODO + public void testLockMany() + { + } + + /** + * Test unlock node + */ + public void testUnlock() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Lock the parent node + testLock(); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Try and unlock a locked node + try + { + this.lockService.unlock(this.parentNode); + // This will pass in the open workd + //fail("A user cannot unlock a node that is currently lock by another user."); + } + catch (UnableToReleaseLockException exception) + { + } + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Unlock the node + this.lockService.unlock(this.parentNode); + assertEquals( + LockStatus.NO_LOCK, + this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + assertEquals( + LockStatus.NO_LOCK, + this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Try and unlock node with no lock + try + { + this.lockService.unlock(this.parentNode); + } + catch (Exception exception) + { + fail("Unlocking an unlocked node should not result in an exception being raised."); + } + + // Test with no apect node + this.lockService.unlock(this.noAspectNode); + } + + // TODO + public void testUnlockChildren() + { + } + + // TODO + public void testUnlockMany() + { + } + + /** + * Test getLockStatus + */ + public void testGetLockStatus() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Check an unlocked node + LockStatus lockStatus1 = this.lockService.getLockStatus(this.parentNode); + assertEquals(LockStatus.NO_LOCK, lockStatus1); + + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Check for locked status + LockStatus lockStatus2 = this.lockService.getLockStatus(this.parentNode); + assertEquals(LockStatus.LOCKED, lockStatus2); + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Check for lock owner status + LockStatus lockStatus3 = this.lockService.getLockStatus(this.parentNode); + assertEquals(LockStatus.LOCK_OWNER, lockStatus3); + + // Test with no apect node + this.lockService.getLockStatus(this.noAspectNode); + + // Test method overload + LockStatus lockStatus4 = this.lockService.getLockStatus(this.parentNode); + assertEquals(LockStatus.LOCK_OWNER, lockStatus4); + } + + public void testGetLocks() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + List locked1 = this.lockService.getLocks(this.storeRef); + assertNotNull(locked1); + assertEquals(0, locked1.size()); + + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK); + this.lockService.lock(this.childNode1, LockType.WRITE_LOCK); + this.lockService.lock(this.childNode2, LockType.READ_ONLY_LOCK); + + List locked2 = this.lockService.getLocks(this.storeRef); + assertNotNull(locked2); + assertEquals(3, locked2.size()); + + List locked3 = this.lockService.getLocks(this.storeRef, LockType.WRITE_LOCK); + assertNotNull(locked3); + assertEquals(2, locked3.size()); + + List locked4 = this.lockService.getLocks(this.storeRef, LockType.READ_ONLY_LOCK); + assertNotNull(locked4); + assertEquals(1, locked4.size()); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + List locked5 = this.lockService.getLocks(this.storeRef); + assertNotNull(locked5); + assertEquals(0, locked5.size()); + } + + /** + * Test getLockType + */ + public void testGetLockType() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + // Get the lock type (should be null since the object is not locked) + LockType lockType1 = this.lockService.getLockType(this.parentNode); + assertNull(lockType1); + + // Lock the object for writing + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK); + LockType lockType2 = this.lockService.getLockType(this.parentNode); + assertNotNull(lockType2); + assertEquals(LockType.WRITE_LOCK, lockType2); + + // Unlock the node + this.lockService.unlock(this.parentNode); + LockType lockType3 = this.lockService.getLockType(this.parentNode); + assertNull(lockType3); + + // Lock the object for read only + this.lockService.lock(this.parentNode, LockType.READ_ONLY_LOCK); + LockType lockType4 = this.lockService.getLockType(this.parentNode); + assertNotNull(lockType4); + assertEquals(LockType.READ_ONLY_LOCK, lockType4); + + // Test with no apect node + this.lockService.getLockType(this.noAspectNode); + } + + public void testTimeToExpire() + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK, 1); + assertEquals(LockStatus.LOCK_OWNER, this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + assertEquals(LockStatus.LOCKED, this.lockService.getLockStatus(this.parentNode)); + + // Wait for 2 second before re-testing the status + try {Thread.sleep(2*1000);} catch (Exception exception){}; + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + assertEquals(LockStatus.LOCK_EXPIRED, this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + assertEquals(LockStatus.LOCK_EXPIRED, this.lockService.getLockStatus(this.parentNode)); + + // Re-lock and then update the time to expire before lock expires + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK, 0); + try + { + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK, 1); + fail("Can not update lock info if not lock owner"); + } + catch (UnableToAquireLockException exception) + { + // Expected + } + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + this.lockService.lock(this.parentNode, LockType.WRITE_LOCK, 1); + assertEquals(LockStatus.LOCK_OWNER, this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + assertEquals(LockStatus.LOCKED, this.lockService.getLockStatus(this.parentNode)); + + // Wait for 2 second before re-testing the status + try {Thread.sleep(2*1000);} catch (Exception exception){}; + + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + assertEquals(LockStatus.LOCK_EXPIRED, this.lockService.getLockStatus(this.parentNode)); + + TestWithUserUtils.authenticateUser(BAD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + assertEquals(LockStatus.LOCK_EXPIRED, this.lockService.getLockStatus(this.parentNode)); + } +} diff --git a/source/java/org/alfresco/repo/lock/LockTestSuite.java b/source/java/org/alfresco/repo/lock/LockTestSuite.java new file mode 100644 index 0000000000..7b2928890a --- /dev/null +++ b/source/java/org/alfresco/repo/lock/LockTestSuite.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.lock; + + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class LockTestSuite extends TestSuite +{ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(LockBehaviourImplTest.class); + suite.addTestSuite(LockServiceImplTest.class); + return suite; + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java new file mode 100644 index 0000000000..bb797f8013 --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -0,0 +1,768 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.model.filefolder; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.QueryParameterDefImpl; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.SearchLanguageConversion; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation of the file/folder-specific service. + * + * @author Derek Hulley + */ +public class FileFolderServiceImpl implements FileFolderService +{ + /** Shallow search for all files */ + private static final String XPATH_QUERY_SHALLOW_FILES = + "./*" + + "[(subtypeOf('" + ContentModel.TYPE_CONTENT + "'))]"; + + /** Shallow search for all folder */ + private static final String XPATH_QUERY_SHALLOW_FOLDERS = + "./*" + + "[not (subtypeOf('" + ContentModel.TYPE_SYSTEM_FOLDER + "'))" + + " and (subtypeOf('" + ContentModel.TYPE_FOLDER + "'))]"; + + /** Shallow search for all files and folders */ + private static final String XPATH_QUERY_SHALLOW_ALL = + "./*" + + "[like(@cm:name, $cm:name, false)" + + " and not (subtypeOf('" + ContentModel.TYPE_SYSTEM_FOLDER + "'))" + + " and (subtypeOf('" + ContentModel.TYPE_FOLDER + "') or subtypeOf('" + ContentModel.TYPE_CONTENT + "'))]"; + + /** Deep search for files and folders with a name pattern */ + private static final String XPATH_QUERY_DEEP_ALL = + ".//*" + + "[like(@cm:name, $cm:name, false)" + + " and not (subtypeOf('" + ContentModel.TYPE_SYSTEM_FOLDER + "'))" + + " and (subtypeOf('" + ContentModel.TYPE_FOLDER + "') or subtypeOf('" + ContentModel.TYPE_CONTENT + "'))]"; + + /** empty parameters */ + private static final QueryParameterDefinition[] PARAMS_EMPTY = new QueryParameterDefinition[0]; + private static final QueryParameterDefinition[] PARAMS_ANY_NAME = new QueryParameterDefinition[1]; + + private static Log logger = LogFactory.getLog(FileFolderServiceImpl.class); + + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + private NodeService nodeService; + private CopyService copyService; + private SearchService searchService; + private ContentService contentService; + private MimetypeService mimetypeService; + + /** + * Default constructor + */ + public FileFolderServiceImpl() + { + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setCopyService(CopyService copyService) + { + this.copyService = copyService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + public void init() + { + PARAMS_ANY_NAME[0] = new QueryParameterDefImpl( + ContentModel.PROP_NAME, + dictionaryService.getDataType(DataTypeDefinition.TEXT), + true, + "%"); + } + + /** + * Helper method to convert node reference instances to file info + * + * @param nodeRefs the node references + * @return Return a list of file info + * @throws InvalidTypeException if the node is not a valid type + */ + private List toFileInfo(List nodeRefs) throws InvalidTypeException + { + List results = new ArrayList(nodeRefs.size()); + for (NodeRef nodeRef : nodeRefs) + { + FileInfo fileInfo = toFileInfo(nodeRef); + results.add(fileInfo); + } + return results; + } + + /** + * Helper method to convert a node reference instance to a file info + */ + private FileInfo toFileInfo(NodeRef nodeRef) throws InvalidTypeException + { + // get the file attributes + Map properties = nodeService.getProperties(nodeRef); + // is it a folder + QName typeQName = nodeService.getType(nodeRef); + boolean isFolder = isFolder(typeQName); + + // construct the file info and add to the results + FileInfo fileInfo = new FileInfoImpl(nodeRef, isFolder, properties); + // done + return fileInfo; + } + + /** + * Ensure that a file or folder with the given name does not already exist + * + * @throws FileExistsException if a same-named file or folder already exists + */ + private void checkExists(NodeRef parentFolderRef, String name) + throws FileExistsException + { + // check for existing file or folder + List existingFileInfos = this.search(parentFolderRef, name, true, true, false); + if (existingFileInfos.size() > 0) + { + throw new FileExistsException(existingFileInfos.get(0)); + } + } + + /** + * Exception when the type is not a valid File or Folder type + * + * @see ContentModel#TYPE_CONTENT + * @see ContentModel#TYPE_FOLDER + * + * @author Derek Hulley + */ + private static class InvalidTypeException extends RuntimeException + { + private static final long serialVersionUID = -310101369475434280L; + + public InvalidTypeException(String msg) + { + super(msg); + } + } + + /** + * Checks the type for whether it is a file or folder. All invalid types + * lead to runtime exceptions. + * + * @param typeQName the type to check + * @return Returns true if the type is a valid folder type, false if it is a file. + * @throws AlfrescoRuntimeException if the type is not handled by this service + */ + private boolean isFolder(QName typeQName) throws InvalidTypeException + { + if (dictionaryService.isSubClass(typeQName, ContentModel.TYPE_FOLDER)) + { + if (dictionaryService.isSubClass(typeQName, ContentModel.TYPE_SYSTEM_FOLDER)) + { + throw new InvalidTypeException("This service should ignore type " + ContentModel.TYPE_SYSTEM_FOLDER); + } + return true; + } + else if (dictionaryService.isSubClass(typeQName, ContentModel.TYPE_CONTENT)) + { + // it is a regular file + return false; + } + else + { + // unhandled type + throw new InvalidTypeException("Type is not handled by this service: " + typeQName); + } + } + + /** + * TODO: Use Lucene search to get file attributes without having to visit the node service + */ + public List list(NodeRef contextNodeRef) + { + // execute the query + List nodeRefs = searchService.selectNodes( + contextNodeRef, + XPATH_QUERY_SHALLOW_ALL, + PARAMS_ANY_NAME, + namespaceService, + false); + // convert the noderefs + List results = toFileInfo(nodeRefs); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Shallow search for files and folders: \n" + + " context: " + contextNodeRef + "\n" + + " results: " + results); + } + return results; + } + + /** + * TODO: Use Lucene search to get file attributes without having to visit the node service + */ + public List listFiles(NodeRef contextNodeRef) + { + // execute the query + List nodeRefs = searchService.selectNodes( + contextNodeRef, + XPATH_QUERY_SHALLOW_FILES, + PARAMS_EMPTY, + namespaceService, + false); + // convert the noderefs + List results = toFileInfo(nodeRefs); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Shallow search for files: \n" + + " context: " + contextNodeRef + "\n" + + " results: " + results); + } + return results; + } + + /** + * TODO: Use Lucene search to get file attributes without having to visit the node service + */ + public List listFolders(NodeRef contextNodeRef) + { + // execute the query + List nodeRefs = searchService.selectNodes( + contextNodeRef, + XPATH_QUERY_SHALLOW_FOLDERS, + PARAMS_EMPTY, + namespaceService, + false); + // convert the noderefs + List results = toFileInfo(nodeRefs); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Shallow search for folders: \n" + + " context: " + contextNodeRef + "\n" + + " results: " + results); + } + return results; + } + + /** + * @see #search(NodeRef, String, boolean, boolean, boolean) + */ + public List search(NodeRef contextNodeRef, String namePattern, boolean includeSubFolders) + { + return search(contextNodeRef, namePattern, true, true, includeSubFolders); + } + + /** + * Full search with all options + */ + public List search( + NodeRef contextNodeRef, + String namePattern, + boolean fileSearch, + boolean folderSearch, + boolean includeSubFolders) + { + // shortcut if the search is requesting nothing + if (!fileSearch && !folderSearch) + { + return Collections.emptyList(); + } + + // if the name pattern is null, then we use the ANY pattern + QueryParameterDefinition[] params = null; + if (namePattern != null) + { + // the interface specifies the Lucene syntax, so perform a conversion + namePattern = SearchLanguageConversion.convert( + SearchLanguageConversion.DEF_LUCENE, + SearchLanguageConversion.DEF_XPATH_LIKE, + namePattern); + + params = new QueryParameterDefinition[1]; + params[0] = new QueryParameterDefImpl( + ContentModel.PROP_NAME, + dictionaryService.getDataType(DataTypeDefinition.TEXT), + true, + namePattern); + } + else + { + params = PARAMS_ANY_NAME; + } + // determine the correct query to use + String query = null; + if (includeSubFolders) + { + query = XPATH_QUERY_DEEP_ALL; + } + else + { + query = XPATH_QUERY_SHALLOW_ALL; + } + // execute the query + List nodeRefs = searchService.selectNodes( + contextNodeRef, + query, + params, + namespaceService, + false); + List results = toFileInfo(nodeRefs); + // eliminate unwanted files/folders + Iterator iterator = results.iterator(); + while (iterator.hasNext()) + { + FileInfo file = iterator.next(); + if (file.isFolder() && !folderSearch) + { + iterator.remove(); + } + else if (!file.isFolder() && !fileSearch) + { + iterator.remove(); + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Deep search: \n" + + " context: " + contextNodeRef + "\n" + + " pattern: " + namePattern + "\n" + + " files: " + fileSearch + "\n" + + " folders: " + folderSearch + "\n" + + " deep: " + includeSubFolders + "\n" + + " results: " + results); + } + return results; + } + + /** + * @see #move(NodeRef, NodeRef, String) + */ + public FileInfo rename(NodeRef sourceNodeRef, String newName) throws FileExistsException, FileNotFoundException + { + return move(sourceNodeRef, null, newName); + } + + /** + * @see #moveOrCopy(NodeRef, NodeRef, String, boolean) + */ + public FileInfo move(NodeRef sourceNodeRef, NodeRef targetParentRef, String newName) throws FileExistsException, FileNotFoundException + { + return moveOrCopy(sourceNodeRef, targetParentRef, newName, true); + } + + /** + * @see #moveOrCopy(NodeRef, NodeRef, String, boolean) + */ + public FileInfo copy(NodeRef sourceNodeRef, NodeRef targetParentRef, String newName) throws FileExistsException, FileNotFoundException + { + return moveOrCopy(sourceNodeRef, targetParentRef, newName, false); + } + + /** + * Implements both move and copy behaviour + * + * @param move true to move, otherwise false to copy + */ + private FileInfo moveOrCopy(NodeRef sourceNodeRef, NodeRef targetParentRef, String newName, boolean move) throws FileExistsException, FileNotFoundException + { + // get file/folder in its current state + FileInfo beforeFileInfo = toFileInfo(sourceNodeRef); + // check the name - null means keep the existing name + if (newName == null) + { + newName = beforeFileInfo.getName(); + } + + // we need the current association type + ChildAssociationRef assocRef = nodeService.getPrimaryParent(sourceNodeRef); + if (targetParentRef == null) + { + targetParentRef = assocRef.getParentRef(); + } + + // there is nothing to do if both the name and parent folder haven't changed + if (targetParentRef.equals(assocRef.getParentRef()) && newName.equals(beforeFileInfo.getName())) + { + if (logger.isDebugEnabled()) + { + logger.debug("Doing nothing - neither filename or parent has not changed: \n" + + " parent: " + targetParentRef + "\n" + + " before: " + beforeFileInfo + "\n" + + " new name: " + newName); + } + return beforeFileInfo; + } + + // check for existing file or folder + checkExists(targetParentRef, newName); + + QName qname = QName.createQName( + NamespaceService.CONTENT_MODEL_1_0_URI, + QName.createValidLocalName(newName)); + + // move or copy + NodeRef targetNodeRef = null; + if (move) + { + // move the node so that the association moves as well + ChildAssociationRef newAssocRef = nodeService.moveNode( + sourceNodeRef, + targetParentRef, + assocRef.getTypeQName(), + qname); + targetNodeRef = newAssocRef.getChildRef(); + } + else + { + // copy the node + targetNodeRef = copyService.copy( + sourceNodeRef, + targetParentRef, + assocRef.getTypeQName(), + qname, + true); + } + // changed the name property + nodeService.setProperty(targetNodeRef, ContentModel.PROP_NAME, newName); + + // get the details after the operation + FileInfo afterFileInfo = toFileInfo(targetNodeRef); + // done + if (logger.isDebugEnabled()) + { + logger.debug("" + (move ? "Moved" : "Copied") + " node: \n" + + " parent: " + targetParentRef + "\n" + + " before: " + beforeFileInfo + "\n" + + " after: " + afterFileInfo); + } + return afterFileInfo; + } + + public FileInfo create(NodeRef parentNodeRef, String name, QName typeQName) throws FileExistsException + { + // file or folder + boolean isFolder = false; + try + { + isFolder = isFolder(typeQName); + } + catch (InvalidTypeException e) + { + throw new AlfrescoRuntimeException("The type is not supported by this service: " + typeQName); + } + + // check for existing file or folder + checkExists(parentNodeRef, name); + + // set up initial properties + Map properties = new HashMap(11); + properties.put(ContentModel.PROP_NAME, (Serializable) name); + if (!isFolder) + { + // guess a mimetype based on the filename + String mimetype = mimetypeService.guessMimetype(name); + ContentData contentData = new ContentData(null, mimetype, 0L, "UTF-8"); + properties.put(ContentModel.PROP_CONTENT, contentData); + } + + // create the node + QName qname = QName.createQName( + NamespaceService.CONTENT_MODEL_1_0_URI, + QName.createValidLocalName(name)); + ChildAssociationRef assocRef = nodeService.createNode( + parentNodeRef, + ContentModel.ASSOC_CONTAINS, + qname, + typeQName, + properties); + NodeRef nodeRef = assocRef.getChildRef(); + FileInfo fileInfo = toFileInfo(nodeRef); + // done + if (logger.isDebugEnabled()) + { + FileInfo parentFileInfo = toFileInfo(parentNodeRef); + logger.debug("Created: \n" + + " parent: " + parentFileInfo + "\n" + + " created: " + fileInfo); + } + return fileInfo; + } + + public void delete(NodeRef nodeRef) + { + nodeService.deleteNode(nodeRef); + } + + public FileInfo makeFolders(NodeRef parentNodeRef, List pathElements, QName folderTypeQName) + { + if (pathElements.size() == 0) + { + throw new IllegalArgumentException("Path element list is empty"); + } + + // make sure that the folder is correct + boolean isFolder = isFolder(folderTypeQName); + if (!isFolder) + { + throw new IllegalArgumentException("Type is invalid to make folders with: " + folderTypeQName); + } + + NodeRef currentParentRef = parentNodeRef; + // just loop and create if necessary + FileInfo lastFileInfo = null; + for (String pathElement : pathElements) + { + try + { + // not present - make it + FileInfo createdFileInfo = create(currentParentRef, pathElement, folderTypeQName); + currentParentRef = createdFileInfo.getNodeRef(); + lastFileInfo = createdFileInfo; + } + catch (FileExistsException e) + { + // it exists - just get it + List fileInfos = search(currentParentRef, pathElement, false, true, false); + if (fileInfos.size() == 0) + { + // ? It must have been removed + throw new AlfrescoRuntimeException("Path element has just been removed: " + pathElement); + } + currentParentRef = fileInfos.get(0).getNodeRef(); + lastFileInfo = fileInfos.get(0); + } + } + // done + return lastFileInfo; + } + + public List getNamePath(NodeRef rootNodeRef, NodeRef nodeRef) throws FileNotFoundException + { + // check the root + if (rootNodeRef == null) + { + rootNodeRef = nodeService.getRootNode(nodeRef.getStoreRef()); + } + try + { + List results = new ArrayList(10); + // get the primary path + Path path = nodeService.getPath(nodeRef); + // iterate and turn the results into file info objects + boolean foundRoot = false; + for (Path.Element element : path) + { + // ignore everything down to the root + Path.ChildAssocElement assocElement = (Path.ChildAssocElement) element; + NodeRef childNodeRef = assocElement.getRef().getChildRef(); + if (childNodeRef.equals(rootNodeRef)) + { + // just found the root - but we don't put in an entry for it + foundRoot = true; + continue; + } + else if (!foundRoot) + { + // keep looking for the root + continue; + } + // we found the root and expect to be building the path up + FileInfo pathInfo = toFileInfo(childNodeRef); + results.add(pathInfo); + } + // check that we found the root + if (!foundRoot || results.size() == 0) + { + throw new FileNotFoundException(nodeRef); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Built name path for node: \n" + + " root: " + rootNodeRef + "\n" + + " node: " + nodeRef + "\n" + + " path: " + results); + } + return results; + } + catch (InvalidNodeRefException e) + { + throw new FileNotFoundException(nodeRef); + } + } + + public FileInfo resolveNamePath(NodeRef rootNodeRef, List pathElements) throws FileNotFoundException + { + if (pathElements.size() == 0) + { + throw new IllegalArgumentException("Path elements list is empty"); + } + // walk the folder tree first + NodeRef parentNodeRef = rootNodeRef; + StringBuilder currentPath = new StringBuilder(pathElements.size() * 20); + int folderCount = pathElements.size() - 1; + for (int i = 0; i < folderCount; i++) + { + String pathElement = pathElements.get(i); + FileInfo pathElementInfo = getPathElementInfo(currentPath, rootNodeRef, parentNodeRef, pathElement, true); + parentNodeRef = pathElementInfo.getNodeRef(); + } + // we have resolved the folder path - resolve the last component + String pathElement = pathElements.get(pathElements.size() - 1); + FileInfo result = getPathElementInfo(currentPath, rootNodeRef, parentNodeRef, pathElement, false); + // found it + if (logger.isDebugEnabled()) + { + logger.debug("Resoved path element: \n" + + " root: " + rootNodeRef + "\n" + + " path: " + currentPath + "\n" + + " node: " + result); + } + return result; + } + + /** + * Helper method to dig down a level for a node based on name + */ + private FileInfo getPathElementInfo( + StringBuilder currentPath, + NodeRef rootNodeRef, + NodeRef parentNodeRef, + String pathElement, + boolean folderOnly) throws FileNotFoundException + { + currentPath.append("/").append(pathElement); + + boolean includeFiles = (folderOnly ? false : true); + List pathElementInfos = search(parentNodeRef, pathElement, includeFiles, true, false); + // check + if (pathElementInfos.size() == 0) + { + StringBuilder sb = new StringBuilder(128); + sb.append(folderOnly ? "Folder" : "File or folder").append(" not found: \n") + .append(" root: ").append(rootNodeRef).append("\n") + .append(" path: ").append(currentPath); + throw new FileNotFoundException(sb.toString()); + } + else if (pathElementInfos.size() > 1) + { + // we have detected a duplicate name - warn, but allow + StringBuilder sb = new StringBuilder(128); + sb.append("Duplicate file or folder found: \n") + .append(" root: ").append(rootNodeRef).append("\n") + .append(" path: ").append(currentPath); + logger.warn(sb); + } + FileInfo pathElementInfo = pathElementInfos.get(0); + return pathElementInfo; + } + + public FileInfo getFileInfo(NodeRef nodeRef) + { + try + { + return toFileInfo(nodeRef); + } + catch (InvalidTypeException e) + { + return null; + } + } + + public ContentReader getReader(NodeRef nodeRef) + { + FileInfo fileInfo = toFileInfo(nodeRef); + if (fileInfo.isFolder()) + { + throw new InvalidTypeException("Unable to get a content reader for a folder: " + fileInfo); + } + return contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + } + + public ContentWriter getWriter(NodeRef nodeRef) + { + FileInfo fileInfo = toFileInfo(nodeRef); + if (fileInfo.isFolder()) + { + throw new InvalidTypeException("Unable to get a content writer for a folder: " + fileInfo); + } + return contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java new file mode 100644 index 0000000000..2de9f9aa7e --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.model.filefolder; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @see org.alfresco.repo.model.filefolder.FileFolderServiceImpl + * + * @author Derek Hulley + */ +public class FileFolderServiceImplTest extends TestCase +{ + private static final String IMPORT_VIEW = "filefolder/filefolder-test-import.xml"; + + private static final String NAME_L0_FILE_A = "L0: File A"; + private static final String NAME_L0_FILE_B = "L0: File B"; + private static final String NAME_L0_FOLDER_A = "L0: Folder A"; + private static final String NAME_L0_FOLDER_B = "L0: Folder B"; + private static final String NAME_L0_FOLDER_C = "L0: Folder C"; + private static final String NAME_L1_FOLDER_A = "L1: Folder A"; + private static final String NAME_L1_FOLDER_B = "L1: Folder B"; + private static final String NAME_L1_FILE_A = "L1: File A"; + private static final String NAME_L1_FILE_B = "L1: File B"; + private static final String NAME_L1_FILE_C = "L1: File C (%_)"; + private static final String NAME_DUPLICATE = "DUPLICATE"; + + private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private TransactionService transactionService; + private NodeService nodeService; + private FileFolderService fileFolderService; + private UserTransaction txn; + private NodeRef rootNodeRef; + private NodeRef workingRootNodeRef; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); + transactionService = serviceRegistry.getTransactionService(); + nodeService = serviceRegistry.getNodeService(); + fileFolderService = serviceRegistry.getFileFolderService(); + AuthenticationComponent authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + + // start the transaction + txn = transactionService.getUserTransaction(); + txn.begin(); + + // authenticate + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + + // create a test store + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, getName() + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + // create a folder to import into + workingRootNodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.ALFRESCO_URI, "working root"), + ContentModel.TYPE_FOLDER).getChildRef(); + + // import the test data + ImporterService importerService = serviceRegistry.getImporterService(); + Location importLocation = new Location(workingRootNodeRef); + InputStream is = getClass().getClassLoader().getResourceAsStream(IMPORT_VIEW); + if (is == null) + { + throw new NullPointerException("Test resource not found: " + IMPORT_VIEW); + } + Reader reader = new InputStreamReader(is); + importerService.importView(reader, importLocation, null, null); + } + + public void tearDown() throws Exception + { + txn.rollback(); + } + + /** + * Checks that the names and numbers of files and folders in the provided list is correct + * + * @param files the list of files + * @param expectedFileCount the number of uniquely named files expected + * @param expectedFolderCount the number of uniquely named folders expected + * @param expectedNames the names of the files and folders expected + */ + private void checkFileList(List files, int expectedFileCount, int expectedFolderCount, String[] expectedNames) + { + int fileCount = 0; + int folderCount = 0; + List check = new ArrayList(8); + for (String filename : expectedNames) + { + check.add(filename); + } + for (FileInfo file : files) + { + if (file.isFolder()) + { + folderCount++; + } + else + { + fileCount++; + } + check.remove(file.getName()); + } + assertTrue("Name list was not exact - remaining: " + check, check.size() == 0); + assertEquals("Incorrect number of files", expectedFileCount, fileCount); + assertEquals("Incorrect number of folders", expectedFolderCount, folderCount); + } + + public void testShallowFilesAndFoldersList() throws Exception + { + List files = fileFolderService.list(workingRootNodeRef); + // check + String[] expectedNames = new String[] {NAME_L0_FILE_A, NAME_L0_FILE_B, NAME_L0_FOLDER_A, NAME_L0_FOLDER_B, NAME_L0_FOLDER_C}; + checkFileList(files, 2, 3, expectedNames); + } + + public void testShallowFilesOnlyList() throws Exception + { + List files = fileFolderService.listFiles(workingRootNodeRef); + // check + String[] expectedNames = new String[] {NAME_L0_FILE_A, NAME_L0_FILE_B}; + checkFileList(files, 2, 0, expectedNames); + } + + public void testShallowFoldersOnlyList() throws Exception + { + List files = fileFolderService.listFolders(workingRootNodeRef); + // check + String[] expectedNames = new String[] {NAME_L0_FOLDER_A, NAME_L0_FOLDER_B, NAME_L0_FOLDER_C}; + checkFileList(files, 0, 3, expectedNames); + } + + public void testShallowFileSearch() throws Exception + { + List files = fileFolderService.search( + workingRootNodeRef, + NAME_L0_FILE_B, + true, + false, + false); + // check + String[] expectedNames = new String[] {NAME_L0_FILE_B}; + checkFileList(files, 1, 0, expectedNames); + } + + public void testDeepFilesAndFoldersSearch() throws Exception + { + List files = fileFolderService.search( + workingRootNodeRef, + "?1:*", + true, + true, + true); + // check + String[] expectedNames = new String[] {NAME_L1_FOLDER_A, NAME_L1_FOLDER_B, NAME_L1_FILE_A, NAME_L1_FILE_B, NAME_L1_FILE_C}; + checkFileList(files, 3, 2, expectedNames); + } + + public void testDeepFilesOnlySearch() throws Exception + { + List files = fileFolderService.search( + workingRootNodeRef, + "?1:*", + true, + false, + true); + // check + String[] expectedNames = new String[] {NAME_L1_FILE_A, NAME_L1_FILE_B, NAME_L1_FILE_C}; + checkFileList(files, 3, 0, expectedNames); + } + + /** + * Helper to fetch a file or folder by name + * + * @param name the name of the file or folder + * @param isFolder true if we want a folder, otherwise false if we want a file + * @return Returns the info for the file or folder + */ + private FileInfo getByName(String name, boolean isFolder) throws Exception + { + List results = fileFolderService.search(workingRootNodeRef, name, !isFolder, isFolder, true); + if (results.size() > 1) + { + throw new AlfrescoRuntimeException("Name is not unique in hierarchy: \n" + + " name: " + name + "\n" + + " is folder: " + isFolder); + } + else if (results.size() == 0) + { + return null; + } + else + { + return results.get(0); + } + } + + /** + * Ensure that an internal method is working - it gets used extensively by following tests + * + * @see #getByName(String, boolean) + */ + public void testGetByName() throws Exception + { + FileInfo fileInfo = getByName(NAME_DUPLICATE, true); + assertNotNull(fileInfo); + assertTrue(fileInfo.isFolder()); + + fileInfo = getByName(NAME_DUPLICATE, false); + assertNotNull(fileInfo); + assertFalse(fileInfo.isFolder()); + } + + public void testRenameNormal() throws Exception + { + FileInfo folderInfo = getByName(NAME_L0_FOLDER_A, true); + assertNotNull(folderInfo); + // rename normal + String newName = "DUPLICATE - renamed"; + folderInfo = fileFolderService.rename(folderInfo.getNodeRef(), newName); + // check it + FileInfo checkInfo = getByName(NAME_L0_FOLDER_A, true); + assertNull("Folder info should have been renamed away", checkInfo); + checkInfo = getByName(newName, true); + assertNotNull("Folder info for new name is not present", checkInfo); + } + + public void testRenameDuplicate() throws Exception + { + FileInfo folderInfo = getByName(NAME_L0_FOLDER_A, true); + assertNotNull(folderInfo); + // rename duplicate. A file with that name already exists + String newName = NAME_L0_FILE_A; + try + { + folderInfo = fileFolderService.rename(folderInfo.getNodeRef(), newName); + fail("Existing file not detected"); + } + catch (FileExistsException e) + { + // expected + } + } + + public void testMove() throws Exception + { + FileInfo folderToMoveInfo = getByName(NAME_L1_FOLDER_A, true); + assertNotNull(folderToMoveInfo); + NodeRef folderToMoveRef = folderToMoveInfo.getNodeRef(); + // move it to the root + fileFolderService.move(folderToMoveRef, workingRootNodeRef, null); + // make sure that it is an immediate child of the root + List checkFileInfos = fileFolderService.search(workingRootNodeRef, NAME_L1_FOLDER_A, false); + assertEquals("Folder not moved to root", 1, checkFileInfos.size()); + // attempt illegal rename (existing) + try + { + fileFolderService.move(folderToMoveRef, null, NAME_L0_FOLDER_A); + fail("Existing folder not detected"); + } + catch (FileExistsException e) + { + // expected + } + // rename properly + FileInfo checkFileInfo = fileFolderService.move(folderToMoveRef, null, "new name"); + checkFileInfos = fileFolderService.search(workingRootNodeRef, checkFileInfo.getName(), false); + assertEquals("Folder not renamed in root", 1, checkFileInfos.size()); + } + + public void testCopy() throws Exception + { + FileInfo folderToCopyInfo = getByName(NAME_L1_FOLDER_A, true); + assertNotNull(folderToCopyInfo); + NodeRef folderToCopyRef = folderToCopyInfo.getNodeRef(); + // copy it to the root + folderToCopyInfo = fileFolderService.copy(folderToCopyRef, workingRootNodeRef, null); + folderToCopyRef = folderToCopyInfo.getNodeRef(); + // make sure that it is an immediate child of the root + List checkFileInfos = fileFolderService.search(workingRootNodeRef, NAME_L1_FOLDER_A, false); + assertEquals("Folder not copied to root", 1, checkFileInfos.size()); + // attempt illegal copy (existing) + try + { + fileFolderService.copy(folderToCopyRef, null, NAME_L0_FOLDER_A); + fail("Existing folder not detected"); + } + catch (FileExistsException e) + { + // expected + } + // copy properly + FileInfo checkFileInfo = fileFolderService.copy(folderToCopyRef, null, "new name"); + checkFileInfos = fileFolderService.search(workingRootNodeRef, checkFileInfo.getName(), false); + assertEquals("Folder not renamed in root", 1, checkFileInfos.size()); + } + + public void testCreateFolder() throws Exception + { + FileInfo parentFolderInfo = getByName(NAME_L0_FOLDER_A, true); + assertNotNull(parentFolderInfo); + NodeRef parentFolderRef = parentFolderInfo.getNodeRef(); + // create a file that already exists + try + { + fileFolderService.create(parentFolderRef, NAME_L1_FILE_A, ContentModel.TYPE_CONTENT); + fail("Failed to detect duplicate filename"); + } + catch (FileExistsException e) + { + // expected + } + // create folder of illegal type + try + { + fileFolderService.create(parentFolderRef, "illegal folder", ContentModel.TYPE_SYSTEM_FOLDER); + fail("Illegal type not detected"); + } + catch (RuntimeException e) + { + // expected + } + // create a file + FileInfo fileInfo = fileFolderService.create(parentFolderRef, "newFile", ContentModel.TYPE_CONTENT); + // check + assertTrue("Node not created", nodeService.exists(fileInfo.getNodeRef())); + assertFalse("File type expected", fileInfo.isFolder()); + } + + public void testCreateInRoot() throws Exception + { + fileFolderService.create(rootNodeRef, "New Folder", ContentModel.TYPE_FOLDER); + } + + public void testMakeFolders() throws Exception + { + // create a completely new path below the root + List namePath = new ArrayList(4); + namePath.add("A"); + namePath.add("B"); + namePath.add("C"); + namePath.add("D"); + + FileInfo lastFileInfo = fileFolderService.makeFolders(rootNodeRef, namePath, ContentModel.TYPE_FOLDER); + assertNotNull("First makeFolder failed", lastFileInfo); + // check that a repeat works + FileInfo lastFileInfoAgain = fileFolderService.makeFolders(rootNodeRef, namePath, ContentModel.TYPE_FOLDER); + assertNotNull("Repeat makeFolders failed", lastFileInfoAgain); + assertEquals("Repeat created new leaf", lastFileInfo.getNodeRef(), lastFileInfoAgain.getNodeRef()); + // check that it worked + List checkInfos = fileFolderService.search(rootNodeRef, "D", false, true, true); + assertEquals("Expected to find a result", 1, checkInfos.size()); + // get the path + List checkPathInfos = fileFolderService.getNamePath(rootNodeRef, checkInfos.get(0).getNodeRef()); + assertEquals("Path created is incorrect", namePath.size(), checkPathInfos.size()); + int i = 0; + for (FileInfo checkInfo : checkPathInfos) + { + assertEquals("Path mismatch", namePath.get(i), checkInfo.getName()); + i++; + } + } + + public void testGetNamePath() throws Exception + { + FileInfo fileInfo = getByName(NAME_L1_FILE_A, false); + assertNotNull(fileInfo); + NodeRef nodeRef = fileInfo.getNodeRef(); + + List infoPaths = fileFolderService.getNamePath(workingRootNodeRef, nodeRef); + assertEquals("Not enough elements", 2, infoPaths.size()); + assertEquals("First level incorrent", NAME_L0_FOLDER_A, infoPaths.get(0).getName()); + assertEquals("Second level incorrent", NAME_L1_FILE_A, infoPaths.get(1).getName()); + + // pass in a null root and make sure that it still works + infoPaths = fileFolderService.getNamePath(null, nodeRef); + assertEquals("Not enough elements", 3, infoPaths.size()); + assertEquals("First level incorrent", workingRootNodeRef.getId(), infoPaths.get(0).getName()); + assertEquals("Second level incorrent", NAME_L0_FOLDER_A, infoPaths.get(1).getName()); + assertEquals("Third level incorrent", NAME_L1_FILE_A, infoPaths.get(2).getName()); + + // check that a non-aligned path is detected + NodeRef startRef = getByName(NAME_L0_FOLDER_B, true).getNodeRef(); + try + { + fileFolderService.getNamePath(startRef, nodeRef); + fail("Failed to detect non-aligned path from root to target node"); + } + catch (FileNotFoundException e) + { + // expected + } + } + + public void testResolveNamePath() throws Exception + { + FileInfo fileInfo = getByName(NAME_L1_FILE_A, false); + List pathElements = new ArrayList(3); + pathElements.add(NAME_L0_FOLDER_A); + pathElements.add(NAME_L1_FILE_A); + + FileInfo fileInfoCheck = fileFolderService.resolveNamePath(workingRootNodeRef, pathElements); + assertNotNull("File info not found", fileInfoCheck); + assertEquals("Path not resolved to correct node", fileInfo.getNodeRef(), fileInfoCheck.getNodeRef()); + } + + public void testGetReaderWriter() throws Exception + { + FileInfo dirInfo = getByName(NAME_L0_FOLDER_A, true); + try + { + fileFolderService.getWriter(dirInfo.getNodeRef()); + fail("Failed to detect content write to folder"); + } + catch (RuntimeException e) + { + // expected + } + + FileInfo fileInfo = getByName(NAME_L1_FILE_A, false); + + ContentWriter writer = fileFolderService.getWriter(fileInfo.getNodeRef()); + assertNotNull("Writer is null", writer); + // write some content + String content = "ABC"; + writer.putContent(content); + // read the content + ContentReader reader = fileFolderService.getReader(fileInfo.getNodeRef()); + assertNotNull("Reader is null", reader); + String checkContent = reader.getContentString(); + assertEquals("Content mismatch", content, checkContent); + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java new file mode 100644 index 0000000000..ca7f70f575 --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.model.filefolder; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.namespace.QName; + +/** + * Common file information implementation. + * + * @author Derek Hulley + */ +public class FileInfoImpl implements FileInfo +{ + private NodeRef nodeRef; + private boolean isFolder; + private Map properties; + + /** + * Package-level constructor + */ + /* package */ FileInfoImpl(NodeRef nodeRef, boolean isFolder, Map properties) + { + this.nodeRef = nodeRef; + this.isFolder = isFolder; + this.properties = properties; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(80); + sb.append("FileInfo") + .append("[name=").append(getName()) + .append(", isFolder=").append(isFolder) + .append(", nodeRef=").append(nodeRef) + .append("]"); + return sb.toString(); + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public boolean isFolder() + { + return isFolder; + } + + public String getName() + { + return (String) properties.get(ContentModel.PROP_NAME); + } + + public Date getCreatedDate() + { + return DefaultTypeConverter.INSTANCE.convert(Date.class, properties.get(ContentModel.PROP_CREATED)); + } + + public Date getModifiedDate() + { + return DefaultTypeConverter.INSTANCE.convert(Date.class, properties.get(ContentModel.PROP_MODIFIED)); + } + + public ContentData getContentData() + { + return DefaultTypeConverter.INSTANCE.convert(ContentData.class, properties.get(ContentModel.PROP_CONTENT)); + } + + public Map getProperties() + { + return properties; + } +} diff --git a/source/java/org/alfresco/repo/model/package.html b/source/java/org/alfresco/repo/model/package.html new file mode 100644 index 0000000000..2e54974422 --- /dev/null +++ b/source/java/org/alfresco/repo/model/package.html @@ -0,0 +1,8 @@ + + + + + +Implementations of model-specific services. + + diff --git a/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java b/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java new file mode 100644 index 0000000000..9a03862c56 --- /dev/null +++ b/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java @@ -0,0 +1,626 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.PropertyValue; +import org.alfresco.repo.node.NodeServicePolicies.BeforeAddAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateChildAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateStorePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteChildAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeRemoveAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeUpdateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateStorePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnDeleteAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnRemoveAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; +import org.alfresco.repo.policy.AssociationPolicyDelegate; +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.search.Indexer; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.GUID; + +/** + * Provides common functionality for + * {@link org.alfresco.service.cmr.repository.NodeService} implementations. + *

    + * Some of the overloaded simpler versions of methods are implemented by passing + * through the defaults as required. + *

    + * The callback handling is also provided as a convenience for implementations. + * + * @author Derek Hulley + */ +public abstract class AbstractNodeServiceImpl implements NodeService +{ + /** a uuid identifying this unique instance */ + private String uuid; + /** controls policy delegates */ + private PolicyComponent policyComponent; + + /* + * Policy delegates + */ + private ClassPolicyDelegate beforeCreateStoreDelegate; + private ClassPolicyDelegate onCreateStoreDelegate; + private ClassPolicyDelegate beforeCreateNodeDelegate; + private ClassPolicyDelegate onCreateNodeDelegate; + private ClassPolicyDelegate beforeUpdateNodeDelegate; + private ClassPolicyDelegate onUpdateNodeDelegate; + private ClassPolicyDelegate onUpdatePropertiesDelegate; + private ClassPolicyDelegate beforeDeleteNodeDelegate; + private ClassPolicyDelegate onDeleteNodeDelegate; + private ClassPolicyDelegate beforeAddAspectDelegate; + private ClassPolicyDelegate onAddAspectDelegate; + private ClassPolicyDelegate beforeRemoveAspectDelegate; + private ClassPolicyDelegate onRemoveAspectDelegate; + private AssociationPolicyDelegate beforeCreateChildAssociationDelegate; + private AssociationPolicyDelegate onCreateChildAssociationDelegate; + private AssociationPolicyDelegate beforeDeleteChildAssociationDelegate; + private AssociationPolicyDelegate onDeleteChildAssociationDelegate; + private AssociationPolicyDelegate onCreateAssociationDelegate; + private AssociationPolicyDelegate onDeleteAssociationDelegate; + + /** + * @param policyComponent the component with which to register class policies and behaviour + * @param dictionaryService + * used to check that node operations conform to the model + */ + protected AbstractNodeServiceImpl(PolicyComponent policyComponent) + { + this.uuid = GUID.generate(); + this.policyComponent = policyComponent; + } + + /** + * Checks equality by type and uuid + */ + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (!(obj instanceof AbstractNodeServiceImpl)) + { + return false; + } + AbstractNodeServiceImpl that = (AbstractNodeServiceImpl) obj; + return this.uuid.equals(that.uuid); + } + + /** + * @see #uuid + */ + public int hashCode() + { + return uuid.hashCode(); + } + + /** + * Registers the node policies as well as node indexing behaviour if the + * {@link #setIndexer(Indexer) indexer} is present. + */ + public void init() + { + // Register the various policies + beforeCreateStoreDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeCreateStorePolicy.class); + onCreateStoreDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnCreateStorePolicy.class); + beforeCreateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeCreateNodePolicy.class); + onCreateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnCreateNodePolicy.class); + beforeUpdateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeUpdateNodePolicy.class); + onUpdateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnUpdateNodePolicy.class); + onUpdatePropertiesDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnUpdatePropertiesPolicy.class); + beforeDeleteNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeDeleteNodePolicy.class); + onDeleteNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnDeleteNodePolicy.class); + + beforeAddAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeAddAspectPolicy.class); + onAddAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnAddAspectPolicy.class); + beforeRemoveAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeRemoveAspectPolicy.class); + onRemoveAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnRemoveAspectPolicy.class); + + beforeCreateChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.BeforeCreateChildAssociationPolicy.class); + onCreateChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnCreateChildAssociationPolicy.class); + beforeDeleteChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.BeforeDeleteChildAssociationPolicy.class); + onDeleteChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnDeleteChildAssociationPolicy.class); + + onCreateAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnCreateAssociationPolicy.class); + onDeleteAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnDeleteAssociationPolicy.class); + } + + /** + * @see NodeServicePolicies.BeforeCreateStorePolicy#beforeCreateStore(QName, + * StoreRef) + */ + protected void invokeBeforeCreateStore(QName nodeTypeQName, StoreRef storeRef) + { + NodeServicePolicies.BeforeCreateStorePolicy policy = this.beforeCreateStoreDelegate.get(nodeTypeQName); + policy.beforeCreateStore(nodeTypeQName, storeRef); + } + + /** + * @see NodeServicePolicies.OnCreateStorePolicy#onCreateStore(NodeRef) + */ + protected void invokeOnCreateStore(NodeRef rootNodeRef) + { + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(rootNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnCreateStorePolicy policy = onCreateStoreDelegate.get(qnames); + policy.onCreateStore(rootNodeRef); + } + + /** + * @see NodeServicePolicies.BeforeCreateNodePolicy#beforeCreateNode(NodeRef, + * QName, QName, QName) + */ + protected void invokeBeforeCreateNode(NodeRef parentNodeRef, QName assocTypeQName, QName assocQName, QName childNodeTypeQName) + { + // execute policy for node type + NodeServicePolicies.BeforeCreateNodePolicy policy = beforeCreateNodeDelegate.get(parentNodeRef, childNodeTypeQName); + policy.beforeCreateNode(parentNodeRef, assocTypeQName, assocQName, childNodeTypeQName); + } + + /** + * @see NodeServicePolicies.OnCreateNodePolicy#onCreateNode(ChildAssociationRef) + */ + protected void invokeOnCreateNode(ChildAssociationRef childAssocRef) + { + NodeRef childNodeRef = childAssocRef.getChildRef(); + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(childNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnCreateNodePolicy policy = onCreateNodeDelegate.get(childNodeRef, qnames); + policy.onCreateNode(childAssocRef); + } + + /** + * @see NodeServicePolicies.BeforeUpdateNodePolicy#beforeUpdateNode(NodeRef) + */ + protected void invokeBeforeUpdateNode(NodeRef nodeRef) + { + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(nodeRef); + // execute policy for node type and aspects + NodeServicePolicies.BeforeUpdateNodePolicy policy = beforeUpdateNodeDelegate.get(nodeRef, qnames); + policy.beforeUpdateNode(nodeRef); + } + + /** + * @see NodeServicePolicies.OnUpdateNodePolicy#onUpdateNode(NodeRef) + */ + protected void invokeOnUpdateNode(NodeRef nodeRef) + { + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(nodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnUpdateNodePolicy policy = onUpdateNodeDelegate.get(nodeRef, qnames); + policy.onUpdateNode(nodeRef); + } + + /** + * @see NodeServicePolicies.OnUpdateProperties#onUpdatePropertiesPolicy(NodeRef, Map, Map) + */ + protected void invokeOnUpdateProperties( + NodeRef nodeRef, + Map before, + Map after) + { + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(nodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnUpdatePropertiesPolicy policy = onUpdatePropertiesDelegate.get(nodeRef, qnames); + policy.onUpdateProperties(nodeRef, before, after); + } + + /** + * @see NodeServicePolicies.BeforeDeleteNodePolicy#beforeDeleteNode(NodeRef) + */ + protected void invokeBeforeDeleteNode(NodeRef nodeRef) + { + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(nodeRef); + // execute policy for node type and aspects + NodeServicePolicies.BeforeDeleteNodePolicy policy = beforeDeleteNodeDelegate.get(nodeRef, qnames); + policy.beforeDeleteNode(nodeRef); + } + + /** + * @see NodeServicePolicies.OnDeleteNodePolicy#onDeleteNode(ChildAssociationRef) + */ + protected void invokeOnDeleteNode(ChildAssociationRef childAssocRef, QName childNodeTypeQName, Set childAspectQnames) + { + // get qnames to invoke against + Set qnames = new HashSet(childAspectQnames.size() + 1); + qnames.addAll(childAspectQnames); + qnames.add(childNodeTypeQName); + + // execute policy for node type and aspects + NodeServicePolicies.OnDeleteNodePolicy policy = onDeleteNodeDelegate.get(childAssocRef.getChildRef(), qnames); + policy.onDeleteNode(childAssocRef); + } + + /** + * @see NodeServicePolicies.BeforeAddAspectPolicy#beforeAddAspect(NodeRef, + * QName) + */ + protected void invokeBeforeAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + NodeServicePolicies.BeforeAddAspectPolicy policy = beforeAddAspectDelegate.get(nodeRef, aspectTypeQName); + policy.beforeAddAspect(nodeRef, aspectTypeQName); + } + + /** + * @see NodeServicePolicies.OnAddAspectPolicy#onAddAspect(NodeRef, QName) + */ + protected void invokeOnAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + NodeServicePolicies.OnAddAspectPolicy policy = onAddAspectDelegate.get(nodeRef, aspectTypeQName); + policy.onAddAspect(nodeRef, aspectTypeQName); + } + + /** + * @see NodeServicePolicies.BeforeRemoveAspectPolicy#BeforeRemoveAspect(NodeRef, + * QName) + */ + protected void invokeBeforeRemoveAspect(NodeRef nodeRef, QName aspectTypeQName) + { + NodeServicePolicies.BeforeRemoveAspectPolicy policy = beforeRemoveAspectDelegate.get(nodeRef, aspectTypeQName); + policy.beforeRemoveAspect(nodeRef, aspectTypeQName); + } + + /** + * @see NodeServicePolicies.OnRemoveAspectPolicy#onRemoveAspect(NodeRef, + * QName) + */ + protected void invokeOnRemoveAspect(NodeRef nodeRef, QName aspectTypeQName) + { + NodeServicePolicies.OnRemoveAspectPolicy policy = onRemoveAspectDelegate.get(nodeRef, aspectTypeQName); + policy.onRemoveAspect(nodeRef, aspectTypeQName); + } + + /** + * @see NodeServicePolicies.BeforeCreateChildAssociationPolicy#beforeCreateChildAssociation(NodeRef, + * NodeRef, QName, QName) + */ + protected void invokeBeforeCreateChildAssociation(NodeRef parentNodeRef, NodeRef childNodeRef, QName assocTypeQName, QName assocQName) + { + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(parentNodeRef); + // execute policy for node type + NodeServicePolicies.BeforeCreateChildAssociationPolicy policy = beforeCreateChildAssociationDelegate.get(parentNodeRef, qnames, assocTypeQName); + policy.beforeCreateChildAssociation(parentNodeRef, childNodeRef, assocTypeQName, assocQName); + } + + /** + * @see NodeServicePolicies.OnCreateChildAssociationPolicy#onCreateChildAssociation(ChildAssociationRef) + */ + protected void invokeOnCreateChildAssociation(ChildAssociationRef childAssocRef) + { + // Get the parent reference and the assoc type qName + NodeRef parentNodeRef = childAssocRef.getParentRef(); + QName assocTypeQName = childAssocRef.getTypeQName(); + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(parentNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnCreateChildAssociationPolicy policy = onCreateChildAssociationDelegate.get(parentNodeRef, qnames, assocTypeQName); + policy.onCreateChildAssociation(childAssocRef); + } + + /** + * @see NodeServicePolicies.BeforeDeleteChildAssociationPolicy#beforeDeleteChildAssociation(ChildAssociationRef) + */ + protected void invokeBeforeDeleteChildAssociation(ChildAssociationRef childAssocRef) + { + NodeRef parentNodeRef = childAssocRef.getParentRef(); + QName assocTypeQName = childAssocRef.getTypeQName(); + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(parentNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.BeforeDeleteChildAssociationPolicy policy = beforeDeleteChildAssociationDelegate.get(parentNodeRef, qnames, assocTypeQName); + policy.beforeDeleteChildAssociation(childAssocRef); + } + + /** + * @see NodeServicePolicies.OnDeleteChildAssociationPolicy#onDeleteChildAssociation(ChildAssociationRef) + */ + protected void invokeOnDeleteChildAssociation(ChildAssociationRef childAssocRef) + { + NodeRef parentNodeRef = childAssocRef.getParentRef(); + QName assocTypeQName = childAssocRef.getTypeQName(); + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(parentNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnDeleteChildAssociationPolicy policy = onDeleteChildAssociationDelegate.get(parentNodeRef, qnames, assocTypeQName); + policy.onDeleteChildAssociation(childAssocRef); + } + + /** + * @see NodeServicePolicies.OnCreateAssociationPolicy#onCreateAssociation(NodeRef, NodeRef, QName) + */ + protected void invokeOnCreateAssociation(AssociationRef nodeAssocRef) + { + NodeRef sourceNodeRef = nodeAssocRef.getSourceRef(); + QName assocTypeQName = nodeAssocRef.getTypeQName(); + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(sourceNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnCreateAssociationPolicy policy = onCreateAssociationDelegate.get(sourceNodeRef, qnames, assocTypeQName); + policy.onCreateAssociation(nodeAssocRef); + } + + /** + * @see NodeServicePolicies.OnDeleteAssociationPolicy#onDeleteAssociation(AssociationRef) + */ + protected void invokeOnDeleteAssociation(AssociationRef nodeAssocRef) + { + NodeRef sourceNodeRef = nodeAssocRef.getSourceRef(); + QName assocTypeQName = nodeAssocRef.getTypeQName(); + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(sourceNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.OnDeleteAssociationPolicy policy = onDeleteAssociationDelegate.get(sourceNodeRef, qnames, assocTypeQName); + policy.onDeleteAssociation(nodeAssocRef); + } + + /** + * Get all aspect and node type qualified names + * + * @param nodeRef + * the node we are interested in + * @return Returns a set of qualified names containing the node type and all + * the node aspects, or null if the node no longer exists + */ + protected Set getTypeAndAspectQNames(NodeRef nodeRef) + { + Set qnames = null; + try + { + Set aspectQNames = getAspects(nodeRef); + QName typeQName = getType(nodeRef); + qnames = new HashSet(aspectQNames.size() + 1); + qnames.addAll(aspectQNames); + qnames.add(typeQName); + } + catch (InvalidNodeRefException e) + { + qnames = Collections.emptySet(); + } + // done + return qnames; + } + + /** + * Generates a GUID for the node using either the creation properties or just by + * generating a value randomly. + * + * @param preCreationProperties the properties that will be applied to the node + * @return Returns the ID to create the node with + */ + protected String generateGuid(Map preCreationProperties) + { + String uuid = (String) preCreationProperties.get(ContentModel.PROP_NODE_UUID); + if (uuid == null) + { + uuid = GUID.generate(); + } + else + { + // remove the property as we don't want to persist it + preCreationProperties.remove(ContentModel.PROP_NODE_UUID); + } + // done + return uuid; + } + + /** + * Remove all properties used by the + * {@link ContentModel#ASPECT_REFERENCEABLE referencable aspect}. + *

    + * This method can be used to ensure that the information already stored + * by the node key is not duplicated by the properties. + * + * @param properties properties to change + */ + protected void removeReferencableProperties(Map properties) + { + properties.remove(ContentModel.PROP_STORE_PROTOCOL); + properties.remove(ContentModel.PROP_STORE_IDENTIFIER); + properties.remove(ContentModel.PROP_NODE_UUID); + } + + /** + * Adds all properties used by the + * {@link ContentModel#ASPECT_REFERENCEABLE referencable aspect}. + *

    + * This method can be used to ensure that the values used by the aspect + * are present as node properties. + *

    + * This method also ensures that the {@link ContentModel#PROP_NAME name property} + * is always present as a property on a node. + * + * @param nodeRef the node reference containing the values required + * @param properties the node properties + */ + protected void addReferencableProperties(NodeRef nodeRef, Map properties) + { + properties.put(ContentModel.PROP_STORE_PROTOCOL, nodeRef.getStoreRef().getProtocol()); + properties.put(ContentModel.PROP_STORE_IDENTIFIER, nodeRef.getStoreRef().getIdentifier()); + properties.put(ContentModel.PROP_NODE_UUID, nodeRef.getId()); + // add the ID as the name, if required + if (properties.get(ContentModel.PROP_NAME) == null) + { + properties.put(ContentModel.PROP_NAME, nodeRef.getId()); + } + } + + /** + * Defers to the pattern matching overload + * + * @see RegexQNamePattern#MATCH_ALL + * @see NodeService#getParentAssocs(NodeRef, QNamePattern, QNamePattern) + */ + public List getParentAssocs(NodeRef nodeRef) throws InvalidNodeRefException + { + return getParentAssocs(nodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL); + } + + /** + * Defers to the pattern matching overload + * + * @see RegexQNamePattern#MATCH_ALL + * @see NodeService#getChildAssocs(NodeRef, QNamePattern, QNamePattern) + */ + public final List getChildAssocs(NodeRef nodeRef) throws InvalidNodeRefException + { + return getChildAssocs(nodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL); + } + + /** + * Helper method to convert the Serializable value into a full, + * persistable {@link PropertyValue}. + *

    + * Where the property definition is null, the value will take on the + * {@link DataTypeDefinition#ANY generic ANY} value. + *

    + * Where the property definition specifies a multi-valued property but the + * value provided is not a collection, the value will be wrapped in a collection. + * + * @param propertyDef the property dictionary definition, may be null + * @param value the value, which will be converted according to the definition - + * may be null + * @return Returns the persistable property value + */ + protected PropertyValue makePropertyValue(PropertyDefinition propertyDef, Serializable value) + { + // get property attributes + QName propertyTypeQName = null; + if (propertyDef == null) // property not recognised + { + // allow it for now - persisting excess properties can be useful sometimes + propertyTypeQName = DataTypeDefinition.ANY; + } + else + { + propertyTypeQName = propertyDef.getDataType().getName(); + // check that multi-valued properties are allowed + boolean isMultiValued = propertyDef.isMultiValued(); + if (isMultiValued && !(value instanceof Collection)) + { + if (value != null) + { + // put the value into a collection + // the implementation gives back a Serializable list + value = (Serializable) Collections.singletonList(value); + } + } + else if (!isMultiValued && (value instanceof Collection)) + { + throw new DictionaryException("A single-valued property may not be a collection: \n" + + " Property: " + propertyDef + "\n" + + " Value: " + value); + } + } + try + { + PropertyValue propertyValue = new PropertyValue(propertyTypeQName, value); + // done + return propertyValue; + } + catch (TypeConversionException e) + { + throw new TypeConversionException( + "The property value is not compatible with the type defined for the property: \n" + + " property: " + (propertyDef == null ? "unknown" : propertyDef) + "\n" + + " value: " + value + "\n" + + " value type: " + value.getClass(), + e); + } + } + + /** + * Extracts the externally-visible property from the {@link PropertyValue propertyValue}. + * + * @param propertyDef + * @param propertyValue + * @return Returns the value of the property in the format dictated by the property + * definition, or null if the property value is null + */ + protected Serializable makeSerializableValue(PropertyDefinition propertyDef, PropertyValue propertyValue) + { + if (propertyValue == null) + { + return null; + } + // get property attributes + QName propertyTypeQName = null; + if (propertyDef == null) + { + // allow this for now + propertyTypeQName = DataTypeDefinition.ANY; + } + else + { + propertyTypeQName = propertyDef.getDataType().getName(); + } + try + { + Serializable value = propertyValue.getValue(propertyTypeQName); + // done + return value; + } + catch (TypeConversionException e) + { + throw new TypeConversionException( + "The property value is not compatible with the type defined for the property: \n" + + " property: " + (propertyDef == null ? "unknown" : propertyDef) + "\n" + + " property value: " + propertyValue, + e); + } + } +} diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java new file mode 100644 index 0000000000..78f3c51ae2 --- /dev/null +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java @@ -0,0 +1,1445 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.DictionaryComponent; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.domain.hibernate.NodeImpl; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidAspectException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationExistsException; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.CyclicChildRelationshipException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; +import org.hibernate.Session; +import org.springframework.context.ApplicationContext; + +/** + * Provides a base set of tests of the various {@link org.alfresco.service.cmr.repository.NodeService} + * implementations. + *

    + * To test a specific incarnation of the service, the methods {@link #getStoreService()} and + * {@link #getNodeService()} must be implemented. + * + * @see #nodeService + * @see #rootNodeRef + * @see #buildNodeGraph() + * + * @author Derek Hulley + */ +public abstract class BaseNodeServiceTest extends BaseSpringTest +{ + public static final String NAMESPACE = "http://www.alfresco.org/test/BaseNodeServiceTest"; + public static final String TEST_PREFIX = "test"; + public static final QName TYPE_QNAME_TEST_CONTENT = QName.createQName(NAMESPACE, "content"); + public static final QName ASPECT_QNAME_TEST_TITLED = QName.createQName(NAMESPACE, "titled"); + public static final QName ASPECT_QNAME_TEST_MARKER = QName.createQName(NAMESPACE, "marker"); + public static final QName ASPECT_QNAME_TEST_MARKER2 = QName.createQName(NAMESPACE, "marker2"); + public static final QName ASPECT_QNAME_MANDATORY = QName.createQName(NAMESPACE, "mandatoryaspect"); + public static final QName PROP_QNAME_TEST_TITLE = QName.createQName(NAMESPACE, "title"); + public static final QName PROP_QNAME_TEST_CONTENT = QName.createQName(NAMESPACE, "content"); + public static final QName ASSOC_TYPE_QNAME_TEST_CHILDREN = ContentModel.ASSOC_CHILDREN; + public static final QName ASSOC_TYPE_QNAME_TEST_NEXT = QName.createQName(NAMESPACE, "next"); + public static final QName TYPE_QNAME_TEST_MANY_PROPERTIES = QName.createQName(NAMESPACE, "many-properties"); + public static final QName PROP_QNAME_BOOLEAN_VALUE = QName.createQName(NAMESPACE, "booleanValue"); + public static final QName PROP_QNAME_INTEGER_VALUE = QName.createQName(NAMESPACE, "integerValue"); + public static final QName PROP_QNAME_LONG_VALUE = QName.createQName(NAMESPACE, "longValue"); + public static final QName PROP_QNAME_FLOAT_VALUE = QName.createQName(NAMESPACE, "floatValue"); + public static final QName PROP_QNAME_DOUBLE_VALUE = QName.createQName(NAMESPACE, "doubleValue"); + public static final QName PROP_QNAME_STRING_VALUE = QName.createQName(NAMESPACE, "stringValue"); + public static final QName PROP_QNAME_DATE_VALUE = QName.createQName(NAMESPACE, "dateValue"); + public static final QName PROP_QNAME_SERIALIZABLE_VALUE = QName.createQName(NAMESPACE, "serializableValue"); + public static final QName PROP_QNAME_NODEREF_VALUE = QName.createQName(NAMESPACE, "nodeRefValue"); + public static final QName PROP_QNAME_QNAME_VALUE = QName.createQName(NAMESPACE, "qnameValue"); + public static final QName PROP_QNAME_CONTENT_VALUE = QName.createQName(NAMESPACE, "contentValue"); + public static final QName PROP_QNAME_PATH_VALUE = QName.createQName(NAMESPACE, "pathValue"); + public static final QName PROP_QNAME_CATEGORY_VALUE = QName.createQName(NAMESPACE, "categoryValue"); + public static final QName PROP_QNAME_NULL_VALUE = QName.createQName(NAMESPACE, "nullValue"); + public static final QName PROP_QNAME_MULTI_VALUE = QName.createQName(NAMESPACE, "multiValue"); + public static final QName TYPE_QNAME_EXTENDED_CONTENT = QName.createQName(NAMESPACE, "extendedcontent"); + public static final QName PROP_QNAME_PROP1 = QName.createQName(NAMESPACE, "prop1"); + public static final QName ASPECT_QNAME_WITH_DEFAULT_VALUE = QName.createQName(NAMESPACE, "withDefaultValue"); + public static final QName PROP_QNAME_PROP2 = QName.createQName(NAMESPACE, "prop2"); + + protected PolicyComponent policyComponent; + protected DictionaryService dictionaryService; + protected TransactionService transactionService; + protected AuthenticationComponent authenticationComponent; + protected NodeDaoService nodeDaoService; + protected NodeService nodeService; + /** populated during setup */ + protected NodeRef rootNodeRef; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + transactionService = (TransactionService) applicationContext.getBean("transactionComponent"); + policyComponent = (PolicyComponent) applicationContext.getBean("policyComponent"); + authenticationComponent = (AuthenticationComponent) applicationContext.getBean("authenticationComponent"); + + authenticationComponent.setSystemUserAsCurrentUser(); + + DictionaryDAO dictionaryDao = (DictionaryDAO) applicationContext.getBean("dictionaryDAO"); + // load the system model + ClassLoader cl = BaseNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + // load the test model + modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml"); + assertNotNull(modelStream); + model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + DictionaryComponent dictionary = new DictionaryComponent(); + dictionary.setDictionaryDAO(dictionaryDao); + dictionaryService = loadModel(applicationContext); + + nodeService = getNodeService(); + + // create a first store directly + StoreRef storeRef = nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + "Test_" + System.nanoTime()); + rootNodeRef = nodeService.getRootNode(storeRef); + } + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + + + /** + * Loads the test model required for building the node graphs + */ + public static DictionaryService loadModel(ApplicationContext applicationContext) + { + DictionaryDAO dictionaryDao = (DictionaryDAO) applicationContext.getBean("dictionaryDAO"); + // load the system model + ClassLoader cl = BaseNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + // load the test model + modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml"); + assertNotNull(modelStream); + model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + DictionaryComponent dictionary = new DictionaryComponent(); + dictionary.setDictionaryDAO(dictionaryDao); + // done + return dictionary; + } + + /** + * Usually just implemented by fetching the bean directly from the bean factory, + * for example: + *

    + *

    +     *      return (NodeService) applicationContext.getBean("dbNodeService");
    +     * 
    + * + * @return Returns the implementation of NodeService to be + * used for this test + */ + protected abstract NodeService getNodeService(); + + public void testSetUp() throws Exception + { + assertNotNull("StoreService not set", nodeService); + assertNotNull("NodeService not set", nodeService); + assertNotNull("rootNodeRef not created", rootNodeRef); + } + + /** + * @see #buildNodeGraph(NodeService, NodeRef) + */ + public Map buildNodeGraph() throws Exception + { + return BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + } + + /** + * Builds a graph of child associations as follows: + *
    +     * Level 0:     root
    +     * Level 1:     root_p_n1   root_p_n2
    +     * Level 2:     n1_p_n3     n2_p_n4     n1_n4       n2_p_n5
    +     * Level 3:     n3_p_n6     n4_n6       n5_p_n7
    +     * Level 4:     n6_p_n8     n7_n8
    +     * 
    + *

    + *

      + *
    • Apart from the root node having the root aspect, node 6 (n6) also has the + * root aspect.
    • + *
    • n3 has properties animal = monkey and + * reference = n2.toString().
    • + *
    • All nodes are of type {@link ContentModel#TYPE_CONTAINER container} + * with the exception of n8, which is of type {@link #TYPE_QNAME_TEST_CONTENT test:content}
    • + *
    + *

    + * The namespace URI for all associations is {@link BaseNodeServiceTest#NAMESPACE}. + *

    + * The naming convention is: + *

    +     * n2_p_n5
    +     * n4_n5
    +     * where
    +     *      n5 is the node number of the node
    +     *      n2 is the primary parent node number
    +     *      n4 is any other non-primary parent
    +     * 
    + *

    + * The session is flushed to ensure that persistence occurs correctly. It is + * cleared to ensure that fetches against the created data are correct. + * + * @return Returns a map ChildAssocRef instances keyed by qualified assoc name + */ + public static Map buildNodeGraph( + NodeService nodeService, + NodeRef rootNodeRef) throws Exception + { + String ns = BaseNodeServiceTest.NAMESPACE; + QName qname = null; + ChildAssociationRef assoc = null; + Map properties = new HashMap(); + Map ret = new HashMap(13); + + // LEVEL 0 + + // LEVEL 1 + qname = QName.createQName(ns, "root_p_n1"); + assoc = nodeService.createNode(rootNodeRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER); + ret.put(qname, assoc); + NodeRef n1 = assoc.getChildRef(); + + qname = QName.createQName(ns, "root_p_n2"); + assoc = nodeService.createNode(rootNodeRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER); + ret.put(qname, assoc); + NodeRef n2 = assoc.getChildRef(); + + // LEVEL 2 + + properties.clear(); + properties.put(QName.createQName(ns, "animal"), "monkey"); + properties.put(QName.createQName(ns, "UPPERANIMAL"), "MONKEY"); + properties.put(QName.createQName(ns, "reference"), n2.toString()); + properties.put(QName.createQName(ns, "text1"), "bun"); + properties.put(QName.createQName(ns, "text2"), "cake"); + properties.put(QName.createQName(ns, "text3"), "biscuit"); + properties.put(QName.createQName(ns, "text12"), "bun, cake"); + properties.put(QName.createQName(ns, "text13"), "bun, biscuit"); + properties.put(QName.createQName(ns, "text23"), "cake, biscuit"); + properties.put(QName.createQName(ns, "text123"), "bun, cake, biscuit"); + ArrayList slist = new ArrayList(); + slist.add("first"); + slist.add("second"); + slist.add("third"); + + properties.put(QName.createQName(ns, "mvp"), slist); + + ArrayList ilist = new ArrayList(); + ilist.add(new Integer(1)); + ilist.add(new Integer(2)); + ilist.add(new Integer(3)); + + properties.put(QName.createQName(ns, "mvi"), ilist); + + qname = QName.createQName(ns, "n1_p_n3"); + assoc = nodeService.createNode(n1, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER, properties); + ret.put(qname, assoc); + NodeRef n3 = assoc.getChildRef(); + + qname = QName.createQName(ns, "n2_p_n4"); + assoc = nodeService.createNode(n2, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER); + ret.put(qname, assoc); + NodeRef n4 = assoc.getChildRef(); + + qname = QName.createQName(ns, "n1_n4"); + assoc = nodeService.addChild(n1, n4, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname); + ret.put(qname, assoc); + + qname = QName.createQName(ns, "n2_p_n5"); + assoc = nodeService.createNode(n2, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER); + ret.put(qname, assoc); + NodeRef n5 = assoc.getChildRef(); + + // LEVEL 3 + qname = QName.createQName(ns, "n3_p_n6"); + assoc = nodeService.createNode(n3, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER); + ret.put(qname, assoc); + NodeRef n6 = assoc.getChildRef(); + nodeService.addAspect(n6, + ContentModel.ASPECT_ROOT, + Collections.emptyMap()); + + qname = QName.createQName(ns, "n4_n6"); + assoc = nodeService.addChild(n4, n6, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname); + ret.put(qname, assoc); + + qname = QName.createQName(ns, "n5_p_n7"); + assoc = nodeService.createNode(n5, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, ContentModel.TYPE_CONTAINER); + ret.put(qname, assoc); + NodeRef n7 = assoc.getChildRef(); + + // LEVEL 4 + properties.clear(); + properties.put(PROP_QNAME_TEST_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null)); + properties.put(PROP_QNAME_TEST_TITLE, "node8"); + qname = QName.createQName(ns, "n6_p_n8"); + assoc = nodeService.createNode(n6, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname, TYPE_QNAME_TEST_CONTENT, properties); + ret.put(qname, assoc); + NodeRef n8 = assoc.getChildRef(); + + qname = QName.createQName(ns, "n7_n8"); + assoc = nodeService.addChild(n7, n8, ASSOC_TYPE_QNAME_TEST_CHILDREN, qname); + ret.put(qname, assoc); + +// // flush and clear +// getSession().flush(); +// getSession().clear(); + + // done + return ret; + } + + private int countNodesById(NodeRef nodeRef) + { + String query = + "select count(node.key.guid)" + + " from " + + NodeImpl.class.getName() + " node" + + " where node.key.guid = ?"; + Session session = getSession(); + List results = session.createQuery(query) + .setString(0, nodeRef.getId()) + .list(); + Integer count = (Integer) results.get(0); + return count.intValue(); + } + + /** + * @return Returns a reference to the created store + */ + private StoreRef createStore() throws Exception + { + StoreRef storeRef = nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + getName() + "_" + System.nanoTime()); + assertNotNull("No reference returned", storeRef); + // done + return storeRef; + } + + public void testCreateStore() throws Exception + { + StoreRef storeRef = createStore(); + + // check that it exists + assertTrue("NodeService reports that store doesn't exist", nodeService.exists(storeRef)); + + // get the root node + NodeRef storeRootNode = nodeService.getRootNode(storeRef); + // make sure that it has the root aspect + boolean isRoot = nodeService.hasAspect(storeRootNode, ContentModel.ASPECT_ROOT); + assertTrue("Root node of store does not have root aspect", isRoot); + // and is of the correct type + QName rootType = nodeService.getType(storeRootNode); + assertEquals("Store root node of incorrect type", ContentModel.TYPE_STOREROOT, rootType); + } + + public void testGetStores() throws Exception + { + StoreRef storeRef = createStore(); + + // get all stores + List storeRefs = nodeService.getStores(); + + // check that the store ref is present + assertTrue("New store not present is list of stores", storeRefs.contains(storeRef)); + } + + public void testExists() throws Exception + { + StoreRef storeRef = createStore(); + boolean exists = nodeService.exists(storeRef); + assertEquals("Exists failed", true, exists); + // create bogus ref + StoreRef bogusRef = new StoreRef("What", "the"); + exists = nodeService.exists(bogusRef); + assertEquals("Exists failed", false, exists); + } + + public void testGetRootNode() throws Exception + { + StoreRef storeRef = createStore(); + // get the root node + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + assertNotNull("No root node reference returned", rootNodeRef); + // get the root node again + NodeRef rootNodeRefCheck = nodeService.getRootNode(storeRef); + assertEquals("Root nodes returned different refs", rootNodeRef, rootNodeRefCheck); + } + + public void testCreateNode() throws Exception + { + ChildAssociationRef assocRef = nodeService.createNode(rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER); + assertEquals("Assoc type qname not set", ASSOC_TYPE_QNAME_TEST_CHILDREN, assocRef.getTypeQName()); + assertEquals("Assoc qname not set", QName.createQName("pathA"), assocRef.getQName()); + NodeRef childRef = assocRef.getChildRef(); + QName checkType = nodeService.getType(childRef); + assertEquals("Child node type incorrect", ContentModel.TYPE_CONTAINER, checkType); + } + + /** + * Tests node creation with a pre-determined {@link ContentModel#PROP_NODE_UUID uuid}. + */ + public void testCreateNodeWithId() throws Exception + { + String uuid = GUID.generate(); + // create a node with an explicit UUID + Map properties = new HashMap(5); + properties.put(ContentModel.PROP_NODE_UUID, uuid); + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER, + properties); + // check it + NodeRef expectedNodeRef = new NodeRef(rootNodeRef.getStoreRef(), uuid); + NodeRef checkNodeRef = assocRef.getChildRef(); + assertEquals("Failed to create node with a chosen ID", expectedNodeRef, checkNodeRef); + } + + public void testGetType() throws Exception + { + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER); + NodeRef nodeRef = assocRef.getChildRef(); + // get the type + QName type = nodeService.getType(nodeRef); + assertEquals("Type mismatch", ContentModel.TYPE_CONTAINER, type); + } + + public void testSetType() throws Exception + { + NodeRef nodeRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("setTypeTest"), + TYPE_QNAME_TEST_CONTENT).getChildRef(); + assertEquals(TYPE_QNAME_TEST_CONTENT, this.nodeService.getType(nodeRef)); + + // Now change the type + this.nodeService.setType(nodeRef, TYPE_QNAME_EXTENDED_CONTENT); + assertEquals(TYPE_QNAME_EXTENDED_CONTENT, this.nodeService.getType(nodeRef)); + } + + /** + * Fills the given property map with some values according to the property definitions on the given class + */ + protected void fillProperties(QName qname, Map properties) + { + ClassDefinition classDef = dictionaryService.getClass(qname); + if (classDef == null) + { + throw new RuntimeException("No such class: " + qname); + } + Map propertyDefs = classDef.getProperties(); + // make up a property value for each property + for (QName propertyName : propertyDefs.keySet()) + { + Serializable value = new Long(System.currentTimeMillis()); + // add it + properties.put(propertyName, value); + } + } + + /** + * Checks that aspects can be added, removed and queried. Failure to detect + * inadequate properties is also checked. + */ + public void testAspects() throws Exception + { + // create a regular base node + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "test-container"), + ContentModel.TYPE_CONTAINER); + NodeRef nodeRef = assocRef.getChildRef(); + // add the content aspect to the node, but don't supply any properties + Map properties = new HashMap(20); + nodeService.addAspect(nodeRef, BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED, properties); + + // get the properties required for the aspect + fillProperties(BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED, properties); + // get the node properties before + Map propertiesBefore = nodeService.getProperties(nodeRef); + // add the aspect + nodeService.addAspect(nodeRef, BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED, properties); + // get the properties after and check + Map propertiesAfter = nodeService.getProperties(nodeRef); + assertEquals("Aspect properties not added", + propertiesBefore.size() + 2, + propertiesAfter.size()); + + // check that we know that the aspect is present + Set aspects = nodeService.getAspects(nodeRef); + assertTrue("Titled aspect not present", + aspects.contains(BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED)); + + // check that hasAspect works + boolean hasAspect = nodeService.hasAspect(nodeRef, BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED); + assertTrue("Aspect not confirmed to be on node", hasAspect); + + // remove the aspect + nodeService.removeAspect(nodeRef, BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED); + hasAspect = nodeService.hasAspect(nodeRef, BaseNodeServiceTest.ASPECT_QNAME_TEST_TITLED); + assertFalse("Aspect not removed from node", hasAspect); + + // check that the associated properties were removed + propertiesAfter = nodeService.getProperties(nodeRef); + assertEquals("Aspect properties not removed", + propertiesBefore.size(), + propertiesAfter.size()); + } + + public void testCreateNodeNoProperties() throws Exception + { + // flush to ensure that the pure JDBC query will work + ChildAssociationRef assocRef = nodeService.createNode(rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("path1"), + ContentModel.TYPE_CONTAINER); + NodeRef nodeRef = assocRef.getChildRef(); + // count the nodes with the given id + int count = countNodesById(nodeRef); + assertEquals("Unexpected number of nodes present", 1, count); + } + + /** + * @see #ASPECT_QNAME_TEST_TITLED + */ + public void testCreateNodeWithProperties() throws Exception + { + Map properties = new HashMap(5); + // fill properties + fillProperties(TYPE_QNAME_TEST_CONTENT, properties); + fillProperties(ASPECT_QNAME_TEST_TITLED, properties); + + // create node for real + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("MyContent"), + TYPE_QNAME_TEST_CONTENT, + properties); + NodeRef nodeRef = assocRef.getChildRef(); + // check that the titled aspect is present + assertTrue("Titled aspect not present", + nodeService.hasAspect(nodeRef, ASPECT_QNAME_TEST_TITLED)); + + // attempt to remove the aspect + try + { + nodeService.removeAspect(nodeRef, ASPECT_QNAME_TEST_TITLED); + fail("Failed to prevent removal of type-required aspect"); + } + catch (InvalidAspectException e) + { + // expected + } + } + + public static class BadOnDeleteNodePolicy implements + NodeServicePolicies.OnDeleteNodePolicy, + NodeServicePolicies.BeforeDeleteNodePolicy + { + private NodeService nodeService; + private List deletedNodeRefs; + + public BadOnDeleteNodePolicy(NodeService nodeService, List deletedNodeRefs) + { + this.nodeService = nodeService; + this.deletedNodeRefs = deletedNodeRefs; + } + + public void beforeDeleteNode(NodeRef nodeRef) + { + // add a new child to the child, i.e. just before it is deleted + ChildAssociationRef assocRef = nodeService.createNode( + nodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pre-delete new child"), + ContentModel.TYPE_CONTAINER); + // set some child node properties + nodeService.setProperty(nodeRef, PROP_QNAME_BOOLEAN_VALUE, "true"); + // add an aspect to the child + nodeService.addAspect(nodeRef, ASPECT_QNAME_TEST_TITLED, null); + } + + public void onDeleteNode(ChildAssociationRef childAssocRef) + { + // add the child to the list + deletedNodeRefs.add(childAssocRef.getChildRef()); + // now perform some nasties on the node's parent, i.e. add a new child + NodeRef parentRef = childAssocRef.getParentRef(); + NodeRef childRef = childAssocRef.getChildRef(); + ChildAssociationRef assocRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("post-delete new child"), + ContentModel.TYPE_CONTAINER); + } + + } + + public void testDelete() throws Exception + { + final List deletedNodeRefs = new ArrayList(5); + + NodeServicePolicies.OnDeleteNodePolicy policy = new BadOnDeleteNodePolicy(nodeService, deletedNodeRefs); + // bind to listen to the deletion of a node + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteNode"), + policy, + new JavaBehaviour(policy, "onDeleteNode")); + + // build the node and commit the node graph + Map assocRefs = buildNodeGraph(nodeService, rootNodeRef); + NodeRef n1Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "root_p_n1")).getChildRef(); + NodeRef n3Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n1_p_n3")).getChildRef(); + NodeRef n4Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n2_p_n4")).getChildRef(); + NodeRef n6Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n3_p_n6")).getChildRef(); + NodeRef n8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")).getChildRef(); + + // delete n1 + nodeService.deleteNode(n1Ref); + assertEquals("Node not directly deleted", 0, countNodesById(n1Ref)); + assertEquals("Node not cascade deleted", 0, countNodesById(n3Ref)); + assertEquals("Node incorrectly cascade deleted", 1, countNodesById(n4Ref)); + assertEquals("Node not cascade deleted", 0, countNodesById(n6Ref)); + assertEquals("Node not cascade deleted", 0, countNodesById(n8Ref)); + + // commit to check + setComplete(); + endTransaction(); + } + + private int countChildrenOfNode(NodeRef nodeRef) + { + String query = + "select node.childAssocs" + + " from " + + NodeImpl.class.getName() + " node" + + " where node.key.guid = ?"; + Session session = getSession(); + List results = session.createQuery(query) + .setString(0, nodeRef.getId()) + .list(); + int count = results.size(); + return count; + } + + public void testAddChild() throws Exception + { + // create a bogus reference + NodeRef bogusChildRef = new NodeRef(rootNodeRef.getStoreRef(), "BOGUS"); + try + { + nodeService.addChild(rootNodeRef, bogusChildRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("BOGUS_PATH")); + fail("Failed to detect invalid child node reference"); + } + catch (InvalidNodeRefException e) + { + // expected + } + NodeRef childNodeRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER).getChildRef(); + int countBefore = countChildrenOfNode(rootNodeRef); + assertEquals("Root children count incorrect", 1, countBefore); + // associate the two nodes + nodeService.addChild( + rootNodeRef, + childNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathB")); + // there should now be 2 child assocs on the root + int countAfter = countChildrenOfNode(rootNodeRef); + assertEquals("Root children count incorrect", 2, countAfter); + + // now attempt to create a cyclical relationship + try + { + nodeService.addChild( + childNodeRef, + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("backToRoot")); + fail("Failed to detect cyclic child relationship during addition of child"); + } + catch (CyclicChildRelationshipException e) + { + // expected + } + } + + public void testRemoveChildByRef() throws Exception + { + NodeRef parentRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("parent_child"), + ContentModel.TYPE_CONTAINER).getChildRef(); + ChildAssociationRef pathARef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER); + NodeRef childARef = pathARef.getChildRef(); + ChildAssociationRef pathBRef = nodeService.addChild( + parentRef, childARef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("pathB")); + ChildAssociationRef pathCRef = nodeService.addChild( + parentRef, childARef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("pathC")); + AssociationRef pathDRef = nodeService.createAssociation( + parentRef, childARef, ASSOC_TYPE_QNAME_TEST_NEXT); + // remove the child - this must cascade + nodeService.removeChild(parentRef, childARef); + + assertFalse("Primary child not deleted", nodeService.exists(childARef)); + assertEquals("Child assocs not removed", + 0, nodeService.getChildAssocs( + parentRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + new RegexQNamePattern(".*", "path*")).size()); + assertEquals("Node assoc not removed", + 0, nodeService.getTargetAssocs(parentRef, RegexQNamePattern.MATCH_ALL).size()); + } + + public void testAddAndRemoveChild() throws Exception + { + ChildAssociationRef pathARef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER); + NodeRef childRef = pathARef.getChildRef(); + // make a duplication, but non-primary, child associaton + nodeService.addChild( + rootNodeRef, + pathARef.getChildRef(), + pathARef.getTypeQName(), + pathARef.getQName()); + // now remove the association - it will cascade to the child + nodeService.removeChild(rootNodeRef, childRef); + } + + public enum TestEnum + { + TEST_ONE, + TEST_TWO + } + + public void testProperties() throws Exception + { + // create a node to play with + ChildAssociationRef assocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("playThing"), + ContentModel.TYPE_CONTAINER); + NodeRef nodeRef = assocRef.getChildRef(); + + QName qnameProperty1 = QName.createQName("PROPERTY1"); + String valueProperty1 = "VALUE1"; + QName qnameProperty2 = QName.createQName("PROPERTY2"); + String valueProperty2 = "VALUE2"; + QName qnameProperty3 = QName.createQName("PROPERTY3"); + QName qnameProperty4 = QName.createQName("PROPERTY4"); + + Map properties = new HashMap(5); + properties.put(qnameProperty1, valueProperty1); + // add some properties to the root node + nodeService.setProperties(nodeRef, properties); + // set a single property + nodeService.setProperty(nodeRef, qnameProperty2, valueProperty2); + // set a null property + nodeService.setProperty(nodeRef, qnameProperty3, null); + // set an enum property + nodeService.setProperty(nodeRef, qnameProperty4, TestEnum.TEST_ONE); + + // force a flush + getSession().flush(); + getSession().clear(); + + // make sure that our integrity allows this + AlfrescoTransactionSupport.flush(); + + // now get them back + Map checkMap = nodeService.getProperties(nodeRef); + assertNotNull("Properties were not set/retrieved", checkMap); + assertNotNull("Name property not set automatically", checkMap.get(ContentModel.PROP_NAME)); + assertEquals("Name property to set to ID of node", nodeRef.getId(), checkMap.get(ContentModel.PROP_NAME)); + assertEquals("Property value incorrect", valueProperty1, checkMap.get(qnameProperty1)); + assertEquals("Property value incorrect", valueProperty2, checkMap.get(qnameProperty2)); + assertTrue("Null property not persisted", checkMap.containsKey(qnameProperty3)); + assertNull("Null value not persisted correctly", checkMap.get(qnameProperty3)); + assertEquals("Enum property not retrieved", TestEnum.TEST_ONE, checkMap.get(qnameProperty4)); + + // get a single property direct from the node + Serializable valueCheck = nodeService.getProperty(nodeRef, qnameProperty2); + assertNotNull("Property value not set", valueCheck); + assertEquals("Property value incorrect", "VALUE2", valueCheck); + + // set the property value to null + try + { + nodeService.setProperty(nodeRef, qnameProperty2, null); + } + catch (IllegalArgumentException e) + { + fail("Null property values are allowed"); + } + // try setting null value as part of complete set + try + { + properties = nodeService.getProperties(nodeRef); + properties.put(qnameProperty1, null); + nodeService.setProperties(nodeRef, properties); + } + catch (IllegalArgumentException e) + { + fail("Null property values are allowed in the map"); + } + } + + /** + * Check that properties go in and come out in the correct format + */ + public void testPropertyTypes() throws Exception + { + ArrayList listProperty = new ArrayList(2); + listProperty.add("ABC"); + listProperty.add("DEF"); + + Path pathProperty = new Path(); + pathProperty.append(new Path.SelfElement()).append(new Path.AttributeElement(TYPE_QNAME_TEST_CONTENT)); + + Map properties = new HashMap(17); + properties.put(PROP_QNAME_BOOLEAN_VALUE, true); + properties.put(PROP_QNAME_INTEGER_VALUE, 123); + properties.put(PROP_QNAME_LONG_VALUE, 123L); + properties.put(PROP_QNAME_FLOAT_VALUE, 123.0F); + properties.put(PROP_QNAME_DOUBLE_VALUE, 123.0); + properties.put(PROP_QNAME_STRING_VALUE, "123.0"); + properties.put(PROP_QNAME_DATE_VALUE, new Date()); + properties.put(PROP_QNAME_SERIALIZABLE_VALUE, "456"); + properties.put(PROP_QNAME_NODEREF_VALUE, rootNodeRef); + properties.put(PROP_QNAME_QNAME_VALUE, TYPE_QNAME_TEST_CONTENT); + properties.put(PROP_QNAME_PATH_VALUE, pathProperty); + properties.put(PROP_QNAME_CONTENT_VALUE, new ContentData("url", "text/plain", 88L, "UTF-8")); + properties.put(PROP_QNAME_CATEGORY_VALUE, rootNodeRef); + properties.put(PROP_QNAME_NULL_VALUE, null); + properties.put(PROP_QNAME_MULTI_VALUE, listProperty); + + // create a new node + NodeRef nodeRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("pathA"), + ContentModel.TYPE_CONTAINER, + properties).getChildRef(); + + // persist + flushAndClear(); + + // get the properties back + Map checkProperties = nodeService.getProperties(nodeRef); + // check + for (QName qname : properties.keySet()) + { + Serializable value = properties.get(qname); + Serializable checkValue = checkProperties.get(qname); + assertEquals("Property mismatch - " + qname, value, checkValue); + } + + // check multi-valued properties are created where necessary + nodeService.setProperty(nodeRef, PROP_QNAME_MULTI_VALUE, "GHI"); + Serializable checkProperty = nodeService.getProperty(nodeRef, PROP_QNAME_MULTI_VALUE); + assertTrue("Property not converted to a Collection", checkProperty instanceof Collection); + assertTrue("Collection doesn't contain value", ((Collection)checkProperty).contains("GHI")); + } + + /** + * Checks that the {@link ContentModel#ASPECT_REFERENCEABLE referencable} properties + * are present + */ + public void testGetReferencableProperties() throws Exception + { + // check individual property retrieval + Serializable wsProtocol = nodeService.getProperty(rootNodeRef, ContentModel.PROP_STORE_PROTOCOL); + Serializable wsIdentifier = nodeService.getProperty(rootNodeRef, ContentModel.PROP_STORE_IDENTIFIER); + Serializable nodeUuid = nodeService.getProperty(rootNodeRef, ContentModel.PROP_NODE_UUID); + + assertNotNull("Workspace Protocol property not present", wsProtocol); + assertNotNull("Workspace Identifier property not present", wsIdentifier); + assertNotNull("Node UUID property not present", nodeUuid); + + assertEquals("Workspace Protocol property incorrect", rootNodeRef.getStoreRef().getProtocol(), wsProtocol); + assertEquals("Workspace Identifier property incorrect", rootNodeRef.getStoreRef().getIdentifier(), wsIdentifier); + assertEquals("Node UUID property incorrect", rootNodeRef.getId(), nodeUuid); + + // check mass property retrieval + Map properties = nodeService.getProperties(rootNodeRef); + assertTrue("Workspace Protocol property not present in map", properties.containsKey(ContentModel.PROP_STORE_PROTOCOL)); + assertTrue("Workspace Identifier property not present in map", properties.containsKey(ContentModel.PROP_STORE_IDENTIFIER)); + assertTrue("Node UUID property not present in map", properties.containsKey(ContentModel.PROP_NODE_UUID)); + } + + public void testGetParentAssocs() throws Exception + { + Map assocRefs = buildNodeGraph(); + ChildAssociationRef n3pn6Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n3_p_n6")); + ChildAssociationRef n5pn7Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n5_p_n7")); + ChildAssociationRef n6pn8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")); + ChildAssociationRef n7n8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n7_n8")); + // get a child node's parents + NodeRef n8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")).getChildRef(); + List parentAssocs = nodeService.getParentAssocs(n8Ref); + assertEquals("Incorrect number of parents", 2, parentAssocs.size()); + assertTrue("Expected assoc not found", parentAssocs.contains(n6pn8Ref)); + assertTrue("Expected assoc not found", parentAssocs.contains(n7n8Ref)); + + // check that we can retrieve the primary parent + ChildAssociationRef primaryParentAssocCheck = nodeService.getPrimaryParent(n8Ref); + assertEquals("Primary parent assoc not retrieved", n6pn8Ref, primaryParentAssocCheck); + + // check that the root node returns a null primary parent + ChildAssociationRef rootNodePrimaryAssoc = nodeService.getPrimaryParent(rootNodeRef); + assertNull("Expected null primary parent for root node", rootNodePrimaryAssoc.getParentRef()); + + // get the parent associations based on pattern + List parentAssocRefsByQName = nodeService.getParentAssocs( + n8Ref, + RegexQNamePattern.MATCH_ALL, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "n7_n8")); + assertEquals("Expected to get exactly one match", 1, parentAssocRefsByQName.size()); + + // get the parent association based on type pattern + List childAssocRefsByTypeQName = nodeService.getChildAssocs( + n8Ref, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + RegexQNamePattern.MATCH_ALL); + } + + public void testGetChildAssocs() throws Exception + { + Map assocRefs = buildNodeGraph(); + NodeRef n1Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE,"root_p_n1")).getChildRef(); + ChildAssociationRef n1pn3Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE,"n1_p_n3")); + ChildAssociationRef n1n4Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE,"n1_n4")); + + // get the parent node's children + List childAssocRefs = nodeService.getChildAssocs(n1Ref); + assertEquals("Incorrect number of children", 2, childAssocRefs.size()); + // checks that the order of the children is correct + assertEquals("First child added to n1 was primary to n3: Order of refs is wrong", + n1pn3Ref, childAssocRefs.get(0)); + assertEquals("Second child added to n1 was to n4: Order of refs is wrong", + n1n4Ref, childAssocRefs.get(1)); + // now set the child ordering explicitly - change the order + nodeService.setChildAssociationIndex(n1pn3Ref, 1); + nodeService.setChildAssociationIndex(n1n4Ref, 0); + + // repeat + childAssocRefs = nodeService.getChildAssocs(n1Ref); + assertEquals("Order of refs is wrong", n1pn3Ref, childAssocRefs.get(1)); + assertEquals("Order of refs is wrong", n1n4Ref, childAssocRefs.get(0)); + + // get the child associations based on pattern + List childAssocRefsByQName = nodeService.getChildAssocs( + n1Ref, + RegexQNamePattern.MATCH_ALL, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "n1_p_n3")); + assertEquals("Expected to get exactly one match", 1, childAssocRefsByQName.size()); + + // get the child association based on type pattern + List childAssocRefsByTypeQName = nodeService.getChildAssocs( + n1Ref, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + RegexQNamePattern.MATCH_ALL); + } + + public void testMoveNode() throws Exception + { + Map assocRefs = buildNodeGraph(); + ChildAssociationRef n2pn4Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n2_p_n4")); + ChildAssociationRef n5pn7Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n5_p_n7")); + ChildAssociationRef n6pn8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")); + NodeRef n4Ref = n2pn4Ref.getChildRef(); + NodeRef n5Ref = n5pn7Ref.getParentRef(); + NodeRef n6Ref = n6pn8Ref.getParentRef(); + NodeRef n8Ref = n6pn8Ref.getChildRef(); + // move n8 to n5 + ChildAssociationRef assocRef = nodeService.moveNode( + n8Ref, + n5Ref, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "n5_p_n8")); + // check that n6 is no longer the parent + List n6ChildRefs = nodeService.getChildAssocs( + n6Ref, + RegexQNamePattern.MATCH_ALL, QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")); + assertEquals("Primary child assoc is still present", 0, n6ChildRefs.size()); + // check that n5 is the parent + ChildAssociationRef checkRef = nodeService.getPrimaryParent(n8Ref); + assertEquals("Primary assoc incorrent", assocRef, checkRef); + + // check that cyclic associations are disallowed + try + { + // n6 is a non-primary child of n4. Move n4 into n6 + nodeService.moveNode( + n4Ref, + n6Ref, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n4")); + fail("Failed to detect cyclic relationship during move"); + } + catch (CyclicChildRelationshipException e) + { + // expected + } + } + + /** + * Creates a named association between two nodes + * + * @return Returns an array of [source real NodeRef][target reference NodeRef][assoc name String] + */ + private AssociationRef createAssociation() throws Exception + { + Map properties = new HashMap(5); + fillProperties(TYPE_QNAME_TEST_CONTENT, properties); + fillProperties(ASPECT_QNAME_TEST_TITLED, properties); + + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(null, "N1"), + TYPE_QNAME_TEST_CONTENT, + properties); + NodeRef sourceRef = childAssocRef.getChildRef(); + childAssocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(null, "N2"), + TYPE_QNAME_TEST_CONTENT, + properties); + NodeRef targetRef = childAssocRef.getChildRef(); + + AssociationRef assocRef = nodeService.createAssociation( + sourceRef, + targetRef, + ASSOC_TYPE_QNAME_TEST_NEXT); + // done + return assocRef; + } + + public void testCreateAndRemoveAssociation() throws Exception + { + AssociationRef assocRef = createAssociation(); + NodeRef sourceRef = assocRef.getSourceRef(); + NodeRef targetRef = assocRef.getTargetRef(); + QName qname = assocRef.getTypeQName(); + try + { + // attempt the association in reverse + nodeService.createAssociation(sourceRef, targetRef, qname); + fail("Incorrect node type not detected"); + } + catch (RuntimeException e) + { + // expected + } + try + { + // attempt repeat + nodeService.createAssociation(sourceRef, targetRef, qname); + fail("Duplicate assocation not detected"); + } + catch (AssociationExistsException e) + { + // expected + } + + // create another + Map properties = new HashMap(5); + fillProperties(TYPE_QNAME_TEST_CONTENT, properties); + fillProperties(ASPECT_QNAME_TEST_TITLED, properties); + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(null, "N3"), + TYPE_QNAME_TEST_CONTENT, + properties); + NodeRef anotherTargetRef = childAssocRef.getChildRef(); + AssociationRef anotherAssocRef = nodeService.createAssociation( + sourceRef, + anotherTargetRef, + ASSOC_TYPE_QNAME_TEST_NEXT); + + // remove assocs + List assocs = nodeService.getTargetAssocs(sourceRef, ASSOC_TYPE_QNAME_TEST_NEXT); + for (AssociationRef assoc : assocs) + { + nodeService.removeAssociation( + assoc.getSourceRef(), + assoc.getTargetRef(), + assoc.getTypeQName()); + } + } + + public void testGetTargetAssocs() throws Exception + { + AssociationRef assocRef = createAssociation(); + NodeRef sourceRef = assocRef.getSourceRef(); + NodeRef targetRef = assocRef.getTargetRef(); + QName qname = assocRef.getTypeQName(); + // get the target assocs + List targetAssocs = nodeService.getTargetAssocs(sourceRef, qname); + assertEquals("Incorrect number of targets", 1, targetAssocs.size()); + assertTrue("Target not found", targetAssocs.contains(assocRef)); + } + + public void testGetSourceAssocs() throws Exception + { + AssociationRef assocRef = createAssociation(); + NodeRef sourceRef = assocRef.getSourceRef(); + NodeRef targetRef = assocRef.getTargetRef(); + QName qname = assocRef.getTypeQName(); + // get the source assocs + List sourceAssocs = nodeService.getSourceAssocs(targetRef, qname); + assertEquals("Incorrect number of source assocs", 1, sourceAssocs.size()); + assertTrue("Source not found", sourceAssocs.contains(assocRef)); + } + + /** + * @see #buildNodeGraph() + */ + public void testGetPath() throws Exception + { + Map assocRefs = buildNodeGraph(); + NodeRef n8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE,"n6_p_n8")).getChildRef(); + + // get the primary node path for n8 + Path path = nodeService.getPath(n8Ref); + assertEquals("Primary path incorrect", + "/{" + BaseNodeServiceTest.NAMESPACE + "}root_p_n1/{" + BaseNodeServiceTest.NAMESPACE + "}n1_p_n3/{" + BaseNodeServiceTest.NAMESPACE + "}n3_p_n6/{" + BaseNodeServiceTest.NAMESPACE + "}n6_p_n8", + path.toString()); + } + + /** + * @see #buildNodeGraph() + */ + public void testGetPaths() throws Exception + { + Map assocRefs = buildNodeGraph(); + NodeRef n1Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "root_p_n1")).getChildRef(); + NodeRef n6Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n3_p_n6")).getChildRef(); + NodeRef n8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")).getChildRef(); + + // get all paths for the root node + Collection paths = nodeService.getPaths(rootNodeRef, false); + assertEquals("Root node must have exactly 1 path", 1, paths.size()); + Path rootPath = paths.toArray(new Path[1])[0]; + assertNotNull("Root node path must have 1 element", rootPath.last()); + assertEquals("Root node path must have 1 element", rootPath.first(), rootPath.last()); + + // get all paths for n8 + paths = nodeService.getPaths(n8Ref, false); + assertEquals("Incorrect path count", 5, paths.size()); // n6 is a root as well + // check that each path element has parent node ref, qname and child node ref + for (Path path : paths) + { + // get the path elements + for (Path.Element element : path) + { + assertTrue("Path element of incorrect type", element instanceof Path.ChildAssocElement); + Path.ChildAssocElement childAssocElement = (Path.ChildAssocElement) element; + ChildAssociationRef ref = childAssocElement.getRef(); + if (childAssocElement != path.first()) + { + // for all but the first element, the parent and assoc qname must be set + assertNotNull("Parent node ref not set", ref.getParentRef()); + assertNotNull("QName not set", ref.getQName()); + } + // all associations must have a child ref + assertNotNull("Child node ref not set", ref.getChildRef()); + } + } + + // get primary path for n8 + paths = nodeService.getPaths(n8Ref, true); + assertEquals("Incorrect path count", 1, paths.size()); + + // check that a cyclic path is detected - make n6_n1 + try + { + nodeService.addChild(n6Ref, n1Ref, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("n6_n1")); + nodeService.getPaths(n6Ref, false); + fail("Cyclic relationship not detected"); + } + catch (CyclicChildRelationshipException e) + { + // expected + } + catch (StackOverflowError e) + { + throw e; + } + } + + public void testPrimaryPathCascadeDelete() throws Exception + { + Map assocRefs = buildNodeGraph(); + NodeRef n1Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "root_p_n1")).getChildRef(); + NodeRef n6Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n3_p_n6")).getChildRef(); + NodeRef n8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")).getChildRef(); + + // delete n1 + nodeService.deleteNode(n1Ref); + // check that the rest disappeared + assertFalse("n6 not cascade deleted", nodeService.exists(n6Ref)); + assertFalse("n8 not cascade deleted", nodeService.exists(n8Ref)); + } + + /** + * Test that default values are set when nodes are created and aspects applied + * + * @throws Exception + */ + public void testDefaultValues() throws Exception + { + NodeRef nodeRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("testDefaultValues"), + TYPE_QNAME_EXTENDED_CONTENT).getChildRef(); + assertEquals("defaultValue", this.nodeService.getProperty(nodeRef, PROP_QNAME_PROP1)); + this.nodeService.addAspect(nodeRef, ASPECT_QNAME_WITH_DEFAULT_VALUE, null); + assertEquals("defaultValue", this.nodeService.getProperty(nodeRef, PROP_QNAME_PROP2)); + + // Ensure that default values do not overrite already set values + Map props = new HashMap(1); + props.put(PROP_QNAME_PROP1, "notDefaultValue"); + NodeRef nodeRef2 = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("testDefaultValues"), + TYPE_QNAME_EXTENDED_CONTENT, + props).getChildRef(); + assertEquals("notDefaultValue", this.nodeService.getProperty(nodeRef2, PROP_QNAME_PROP1)); + Map prop2 = new HashMap(1); + prop2.put(PROP_QNAME_PROP2, "notDefaultValue"); + this.nodeService.addAspect(nodeRef2, ASPECT_QNAME_WITH_DEFAULT_VALUE, prop2); + assertEquals("notDefaultValue", this.nodeService.getProperty(nodeRef2, PROP_QNAME_PROP2)); + + } + + public void testMandatoryAspects() + { + NodeRef nodeRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("testDefaultValues"), + TYPE_QNAME_TEST_CONTENT).getChildRef(); + + // Check that the required mandatory aspects have been applied + assertTrue(this.nodeService.hasAspect(nodeRef, ASPECT_QNAME_TEST_TITLED)); + assertTrue(this.nodeService.hasAspect(nodeRef, ASPECT_QNAME_MANDATORY)); + + // Add an aspect with dependacies + this.nodeService.addAspect(nodeRef, ASPECT_QNAME_TEST_MARKER, null); + + // Check that the dependant aspect has been applied + assertTrue(this.nodeService.hasAspect(nodeRef, ASPECT_QNAME_TEST_MARKER)); + assertTrue(this.nodeService.hasAspect(nodeRef, ASPECT_QNAME_TEST_MARKER2)); + } + + private void garbageCollect() throws Exception + { + // garbage collect and wait + for (int i = 0; i < 50; i++) + { + Runtime.getRuntime().gc(); + synchronized(this) + { + this.wait(20); + } + } + } + + private void reportFlushPerformance( + String msg, + Map lastNodeGraph, + int testCount, + long startBytes, + long startTime) throws Exception + { + long endTime = System.nanoTime(); + double deltaTime = (double)(endTime - startTime)/1000000000D; + System.out.println(msg + "\n" + + " Build and flushed " + testCount + " node graphs: \n" + + " total time: " + deltaTime + "s \n" + + " average: " + (double)testCount/deltaTime + " graphs/s"); + + garbageCollect(); + long endBytes = Runtime.getRuntime().freeMemory(); + double diffBytes = (double)(startBytes - endBytes); + System.out.println( + " total bytes: " + diffBytes/1024D/1024D + " MB \n" + + " average: " + (double)diffBytes/testCount/1024D + " kb/graph"); + + + int assocsPerGraph = lastNodeGraph.size(); + int nodesPerGraph = 0; + for (ChildAssociationRef assoc : lastNodeGraph.values()) + { + if (assoc.getQName().toString().contains("_p_")) + { + nodesPerGraph++; + } + } + int totalAssocs = assocsPerGraph * testCount; + int totalNodes = nodesPerGraph * testCount; + System.out.println( + " assocs per graph: " + assocsPerGraph + "\n" + + " nodes per graph: " + nodesPerGraph + "\n" + + " total nodes: " + totalNodes + "\n" + + " total assocs: " + totalAssocs); + } + + /** + * Builds N node graphs, flushing after each build. Checks that memory is being cleared + * adequately. + *

    + * This is also a good test of performance, so that is dumped. + * + * @see BaseNodeServiceTest#buildNodeGraph() + */ + public void testFlush() throws Exception + { + setComplete(); + endTransaction(); + + final int testCount = 500; + + garbageCollect(); + + final long startBytes = Runtime.getRuntime().freeMemory(); + final long startTime = System.nanoTime(); + + TransactionWork> buildWork = new TransactionWork>() + { + public Map doWork() + { + Map nodeGraph = Collections.emptyMap(); + try + { + for (int i = 0; i < testCount; i++) + { + nodeGraph = buildNodeGraph(); + AlfrescoTransactionSupport.flush(); + } + + // report + reportFlushPerformance("Statistics pre-commit", nodeGraph, testCount, startBytes, startTime); + } + catch (OutOfMemoryError e) + { + fail("Flush not clearing memory"); + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Node graph building failed", e); + } + return nodeGraph; + } + }; + Map nodeGraph = TransactionUtil.executeInNonPropagatingUserTransaction(transactionService, buildWork); + + // report post-commit stats + reportFlushPerformance("Statistics post-commit", nodeGraph, testCount, startBytes, startTime); + } +} diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest_model.xml b/source/java/org/alfresco/repo/node/BaseNodeServiceTest_model.xml new file mode 100644 index 0000000000..cee04d7a18 --- /dev/null +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest_model.xml @@ -0,0 +1,294 @@ + + + Test Model for NodeService tests + Alfresco + 2005-06-05 + 0.1 + + + + + + + + + + + + + Content + sys:base + + + d:content + true + + false + false + true + + + + + + + false + false + + + sys:base + false + true + + false + + + + false + false + + + test:content + false + true + + + + + test:titled + + + + + Extended Content + test:content + + + d:text + true + defaultValue + + + + + + MultiProp + sys:base + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + d:text + false + + + d:content + false + + + + + + false + false + + + sys:base + false + true + + false + + + + + + Busy + sys:base + + + d:boolean + true + + + d:int + true + + + d:long + true + + + d:float + true + + + d:double + true + + + d:text + true + + + d:date + true + + + d:any + true + + + d:noderef + true + + + d:qname + true + + + d:content + true + + + d:path + true + + + d:category + true + + + d:text + true + + + d:text + true + true + + + + + + + + + Titled + + + d:text + true + + false + false + true + + + + d:text + + + + test:mandatoryaspect + + + + + Marker Aspect + + test:marker2 + + + + + Marker Aspect 2 + + + + Mandatory Aspect + + + + Marker Aspect + + + d:text + defaultValue + + + + + + + diff --git a/source/java/org/alfresco/repo/node/ConcurrentNodeServiceTest.java b/source/java/org/alfresco/repo/node/ConcurrentNodeServiceTest.java new file mode 100644 index 0000000000..6879f99af1 --- /dev/null +++ b/source/java/org/alfresco/repo/node/ConcurrentNodeServiceTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node; + +import java.io.InputStream; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.apache.lucene.index.IndexWriter; +import org.springframework.context.ApplicationContext; + +public class ConcurrentNodeServiceTest extends TestCase +{ + public static final String NAMESPACE = "http://www.alfresco.org/test/BaseNodeServiceTest"; + public static final String TEST_PREFIX = "test"; + public static final QName TYPE_QNAME_TEST_CONTENT = QName.createQName(NAMESPACE, "content"); + public static final QName ASPECT_QNAME_TEST_TITLED = QName.createQName(NAMESPACE, "titled"); + public static final QName PROP_QNAME_TEST_TITLE = QName.createQName(NAMESPACE, "title"); + public static final QName PROP_QNAME_TEST_MIMETYPE = QName.createQName(NAMESPACE, "mimetype"); + + static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private NodeService nodeService; + private TransactionService transactionService; + private NodeRef rootNodeRef; + private FullTextSearchIndexer luceneFTS; + + private AuthenticationComponent authenticationComponent; + + public ConcurrentNodeServiceTest() + { + super(); + } + + protected void setUp() throws Exception + { + DictionaryDAO dictionaryDao = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + // load the system model + ClassLoader cl = BaseNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("alfresco/model/systemModel.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + // load the test model + modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml"); + assertNotNull(modelStream); + model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + nodeService = (NodeService) ctx.getBean("dbNodeService"); + transactionService = (TransactionService) ctx.getBean("transactionComponent"); + luceneFTS = (FullTextSearchIndexer) ctx.getBean("LuceneFullTextSearchIndexer"); + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + // create a first store directly + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + tx.commit(); + } + + @Override + protected void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + super.tearDown(); + } + + protected Map buildNodeGraph() throws Exception + { + return BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + } + + protected Map commitNodeGraph() throws Exception + { + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + Map answer = buildNodeGraph(); + tx.commit(); + + return null;// answer; + } + + public void testConcurrent() throws Exception + { + luceneFTS.pause(); + IndexWriter.COMMIT_LOCK_TIMEOUT = 100000; + int count = 10; + int repeats = 10; + + Map assocRefs = commitNodeGraph(); + Thread runner = null; + + for (int i = 0; i < count; i++) + { + runner = new Nester("Concurrent-" + i, runner, repeats); + } + if (runner != null) + { + runner.start(); + + try + { + runner.join(); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + SearchService searcher = (SearchService) ctx.getBean(ServiceRegistry.SEARCH_SERVICE.getLocalName()); + assertEquals(2 * ((count * repeats) + 1), searcher.selectNodes(rootNodeRef, "/*", null, + getNamespacePrefixReolsver(""), false).size()); + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*\""); + // n6 has root aspect - there are three things at the root level in the + // index + assertEquals(3 * ((count * repeats) + 1), results.length()); + results.close(); + } + + /** + * Daemon thread + */ + class Nester extends Thread + { + Thread waiter; + + int repeats; + + Nester(String name, Thread waiter, int repeats) + { + super(name); + this.setDaemon(true); + this.waiter = waiter; + this.repeats = repeats; + } + + public void run() + { + if (waiter != null) + { + waiter.start(); + } + try + { + System.out.println("Start " + this.getName()); + for (int i = 0; i < repeats; i++) + { + Map assocRefs = commitNodeGraph(); + System.out.println(" " + this.getName() + " " + i); + } + System.out.println("End " + this.getName()); + } + catch (Exception e) + { + e.printStackTrace(); + System.exit(12); + } + if (waiter != null) + { + try + { + waiter.join(); + } + catch (InterruptedException e) + { + } + } + } + + } + + private NamespacePrefixResolver getNamespacePrefixReolsver(String defaultURI) + { + DynamicNamespacePrefixResolver nspr = new DynamicNamespacePrefixResolver(null); + nspr.registerNamespace(NamespaceService.SYSTEM_MODEL_PREFIX, NamespaceService.SYSTEM_MODEL_1_0_URI); + nspr.registerNamespace(NamespaceService.CONTENT_MODEL_PREFIX, NamespaceService.CONTENT_MODEL_1_0_URI); + nspr.registerNamespace(NamespaceService.APP_MODEL_PREFIX, NamespaceService.APP_MODEL_1_0_URI); + nspr.registerNamespace("namespace", "namespace"); + nspr.registerNamespace(NamespaceService.DEFAULT_PREFIX, defaultURI); + return nspr; + } +} diff --git a/source/java/org/alfresco/repo/node/NodeServicePolicies.java b/source/java/org/alfresco/repo/node/NodeServicePolicies.java new file mode 100644 index 0000000000..0ad8e9210f --- /dev/null +++ b/source/java/org/alfresco/repo/node/NodeServicePolicies.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.policy.AssociationPolicy; +import org.alfresco.repo.policy.ClassPolicy; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + +/** + * Node service policies + * + * @author Roy Wetherall + */ +public interface NodeServicePolicies +{ + public interface BeforeCreateStorePolicy extends ClassPolicy + { + /** + * Called before a new node store is created. + * + * @param nodeTypeQName the type of the node that will be used for the root + * @param storeRef the reference to the store about to be created + */ + public void beforeCreateStore(QName nodeTypeQName, StoreRef storeRef); + } + + public interface OnCreateStorePolicy extends ClassPolicy + { + /** + * Called when a new node store has been created. + * + * @param rootNodeRef the reference to the newly created root node + */ + public void onCreateStore(NodeRef rootNodeRef); + } + + public interface BeforeCreateNodePolicy extends ClassPolicy + { + /** + * Called before a new node is created. + * + * @param parentRef the parent node reference + * @param assocTypeQName the association type qualified name + * @param assocQName the association qualified name + * @param nodeTypeQName the node type qualified name + */ + public void beforeCreateNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName); + } + + public interface OnCreateNodePolicy extends ClassPolicy + { + /** + * Called when a new node has been created. + * + * @param childAssocRef the created child association reference + */ + public void onCreateNode(ChildAssociationRef childAssocRef); + } + + public interface BeforeUpdateNodePolicy extends ClassPolicy + { + /** + * Called before a node is updated. This includes the modification of properties, child and target + * associations and the addition of aspects. + * + * @param nodeRef reference to the node being updated + */ + public void beforeUpdateNode(NodeRef nodeRef); + } + + public interface OnUpdateNodePolicy extends ClassPolicy + { + /** + * Called after a new node has been created. This includes the modification of properties, child and target + * associations and the addition of aspects. + * + * @param nodeRef reference to the updated node + */ + public void onUpdateNode(NodeRef nodeRef); + } + + public interface OnUpdatePropertiesPolicy extends ClassPolicy + { + /** + * Called after a node's properties have been changed. + * + * @param nodeRef reference to the updated node + * @param before the node's properties before the change + * @param after the node's properties after the change + */ + public void onUpdateProperties( + NodeRef nodeRef, + Map before, + Map after); + } + + public interface BeforeDeleteNodePolicy extends ClassPolicy + { + /** + * Called before a node is deleted. + * + * @param nodeRef the node reference + */ + public void beforeDeleteNode(NodeRef nodeRef); + } + + public interface OnDeleteNodePolicy extends ClassPolicy + { + /** + * Called after a node is deleted. The reference given is for an association + * which has been deleted and cannot be used to retrieve node or associaton + * information from any of the services. + * + * @param childAssocRef the primary parent-child association of the deleted node + */ + public void onDeleteNode(ChildAssociationRef childAssocRef); + } + + public interface BeforeAddAspectPolicy extends ClassPolicy + { + /** + * Called before an aspect is added to a node + * + * @param nodeRef the node to which the aspect will be added + * @param aspectTypeQName the type of the aspect + */ + public void beforeAddAspect(NodeRef nodeRef, QName aspectTypeQName); + } + + public interface OnAddAspectPolicy extends ClassPolicy + { + /** + * Called after an aspect has been added to a node + * + * @param nodeRef the node to which the aspect was added + * @param aspectTypeQName the type of the aspect + */ + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName); + } + + public interface BeforeRemoveAspectPolicy extends ClassPolicy + { + /** + * Called before an aspect is removed from a node + * + * @param nodeRef the node from which the aspect will be removed + * @param aspectTypeQName the type of the aspect + */ + public void beforeRemoveAspect(NodeRef nodeRef, QName aspectTypeQName); + } + + public interface OnRemoveAspectPolicy extends ClassPolicy + { + /** + * Called after an aspect has been removed from a node + * + * @param nodeRef the node from which the aspect will be removed + * @param aspectTypeQName the type of the aspect + */ + public void onRemoveAspect(NodeRef nodeRef, QName aspectTypeQName); + } + + public interface BeforeCreateChildAssociationPolicy extends AssociationPolicy + { + /** + * Called before a node child association is created. + * + * @param parentNodeRef + * @param childNodeRef + * @param assocTypeQName the type of the association + * @param assocQName the name of the association + */ + public void beforeCreateChildAssociation( + NodeRef parentNodeRef, + NodeRef childNodeRef, + QName assocTypeQName, + QName assocQName); + } + + public interface OnCreateChildAssociationPolicy extends AssociationPolicy + { + /** + * Called after a node child association has been created. + * + * @param childAssocRef the child association that has been created + */ + public void onCreateChildAssociation(ChildAssociationRef childAssocRef); + } + + public interface BeforeDeleteChildAssociationPolicy extends AssociationPolicy + { + /** + * Called before a node child association is deleted. + * + * @param childAssocRef the child association to be deleted + */ + public void beforeDeleteChildAssociation(ChildAssociationRef childAssocRef); + } + + public interface OnDeleteChildAssociationPolicy extends AssociationPolicy + { + /** + * Called after a node child association has been deleted. + * + * @param childAssocRef the child association that has been deleted + */ + public void onDeleteChildAssociation(ChildAssociationRef childAssocRef); + } + + public interface OnCreateAssociationPolicy extends AssociationPolicy + { + /** + * Called after a regular node association is created. + * + * @param nodeAssocRef the regular node association that was created + */ + public void onCreateAssociation(AssociationRef nodeAssocRef); + } + + public interface OnDeleteAssociationPolicy extends AssociationPolicy + { + /** + * Called after a regular node association is deleted. + * + * @param nodeAssocRef the regular node association that was removed + */ + public void onDeleteAssociation(AssociationRef nodeAssocRef); + } +} diff --git a/source/java/org/alfresco/repo/node/PerformanceNodeServiceTest.java b/source/java/org/alfresco/repo/node/PerformanceNodeServiceTest.java new file mode 100644 index 0000000000..87eb361b74 --- /dev/null +++ b/source/java/org/alfresco/repo/node/PerformanceNodeServiceTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.DictionaryComponent; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; + +/** + * PerformanceNodeServiceTest + */ +public class PerformanceNodeServiceTest extends TestCase +{ + public static final String NAMESPACE = "http://www.alfresco.org/test/BaseNodeServiceTest"; + public static final String TEST_PREFIX = "test"; + public static final QName TYPE_QNAME_TEST = QName.createQName(NAMESPACE, "multiprop"); + public static final QName PROP_QNAME_NAME = QName.createQName(NAMESPACE, "name"); + public static final QName ASSOC_QNAME_CHILDREN = QName.createQName(NAMESPACE, "child"); + + private int flushCount = Integer.MAX_VALUE; + + private int testDepth = 3; + private int testChildCount = 5; + private int testStringPropertyCount = 10; + private int testContentPropertyCount = 10; + + private static Log logger = LogFactory.getLog(PerformanceNodeServiceTest.class); + private static ApplicationContext applicationContext = ApplicationContextHelper.getApplicationContext(); + + protected DictionaryService dictionaryService; + protected NodeService nodeService; + private ContentService contentService; + private TransactionService txnService; + + private int nodeCount = 0; + + private long startTime; + /** populated during setup */ + protected NodeRef rootNodeRef; + + @Override + protected void setUp() throws Exception + { + DictionaryDAO dictionaryDao = (DictionaryDAO) applicationContext.getBean("dictionaryDAO"); + + // load the system model + ClassLoader cl = PerformanceNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + // load the test model + modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml"); + assertNotNull(modelStream); + model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + DictionaryComponent dictionary = new DictionaryComponent(); + dictionary.setDictionaryDAO(dictionaryDao); + dictionaryService = loadModel(applicationContext); + + nodeService = (NodeService) applicationContext.getBean("nodeService"); + txnService = (TransactionService) applicationContext.getBean("transactionComponent"); + contentService = (ContentService) applicationContext.getBean("contentService"); + + // create a first store directly + TransactionWork createStoreWork = new TransactionWork() + { + public NodeRef doWork() + { + StoreRef storeRef = nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + "Test_" + System.nanoTime()); + return nodeService.getRootNode(storeRef); + } + }; + rootNodeRef = TransactionUtil.executeInUserTransaction(txnService, createStoreWork); + } + + @Override + protected void tearDown() + { + } + + /** + * Loads the test model required for building the node graphs + */ + public static DictionaryService loadModel(ApplicationContext applicationContext) + { + DictionaryDAO dictionaryDao = (DictionaryDAO) applicationContext.getBean("dictionaryDAO"); + + // load the system model + ClassLoader cl = PerformanceNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + // load the test model + modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml"); + assertNotNull(modelStream); + model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + DictionaryComponent dictionary = new DictionaryComponent(); + dictionary.setDictionaryDAO(dictionaryDao); + + return dictionary; + } + + public void testSetUp() throws Exception + { + assertNotNull("StoreService not set", nodeService); + assertNotNull("NodeService not set", nodeService); + assertNotNull("rootNodeRef not created", rootNodeRef); + } + + public void testPerformanceNodeService() throws Exception + { + startTime = System.currentTimeMillis(); + + // ensure that we execute the node tree building in a transaction + TransactionWork buildChildrenWork = new TransactionWork() + { + public Object doWork() + { + buildNodeChildren(rootNodeRef, 1, testDepth, testChildCount); + return null; + } + }; + TransactionUtil.executeInUserTransaction(txnService, buildChildrenWork); + + long endTime = System.currentTimeMillis(); + + System.out.println("Test completed: \n" + + " Built " + nodeCount + " nodes in " + (endTime-startTime) + "ms \n" + + " Depth: " + testDepth + "\n" + + " Child count: " + testChildCount); + } + + public void buildNodeChildren(NodeRef parent, int level, int maxLevel, int childCount) + { + for (int i=0; i < childCount; i++) + { + ChildAssociationRef assocRef = this.nodeService.createNode( + parent, ASSOC_QNAME_CHILDREN, QName.createQName(NAMESPACE, "child" + i), TYPE_QNAME_TEST); + + nodeCount++; + + NodeRef childRef = assocRef.getChildRef(); + + this.nodeService.setProperty(childRef, + ContentModel.PROP_NAME, "node" + level + "_" + i); + + Map properties = new HashMap(17); + for (int j = 0; j < testStringPropertyCount; j++) + { + properties.put( + QName.createQName(NAMESPACE, "string" + j), + level + "_" + i + "_" + j); + } + this.nodeService.setProperties(childRef, properties); + + for (int j = 0; j < testContentPropertyCount; j++) + { + ContentWriter writer = this.contentService.getWriter( + childRef, QName.createQName(NAMESPACE, "content" + j), true); + + writer.setMimetype("text/plain"); + writer.putContent( level + "_" + i + "_" + j ); + } + + long currentTime = System.currentTimeMillis(); + long diffTime = (currentTime - startTime); + if (nodeCount % flushCount == 0) + { + System.out.println("Flushing transaction cache at nodecount: " + nodeCount); + System.out.println("At time index " + diffTime + "ms"); + AlfrescoTransactionSupport.flush(); + } + if (nodeCount % 100 == 0) + { + System.out.println("Interim summary: \n" + + " nodes: " + nodeCount + "\n" + + " time: " + (double)diffTime/1000.0/60.0 + " minutes \n" + + " average: " + (double)nodeCount/(double)diffTime*1000.0 + " nodes/s"); + } + + if (level < maxLevel) + { + buildNodeChildren(childRef, level + 1, maxLevel, childCount); + } + } + } + + /** + * Runs a test with more depth + */ + public static void main(String[] args) + { + try + { + PerformanceNodeServiceTest test = new PerformanceNodeServiceTest(); + test.setUp(); + test.testChildCount = 5; + test.testDepth = 6; + test.flushCount = 1000; + + test.testPerformanceNodeService(); + + test.tearDown(); + } + catch (Throwable e) + { + e.printStackTrace(); + System.exit(1); + } + System.exit(0); + } +} diff --git a/source/java/org/alfresco/repo/node/ReferenceableAspect.java b/source/java/org/alfresco/repo/node/ReferenceableAspect.java new file mode 100644 index 0000000000..ca0a45e84d --- /dev/null +++ b/source/java/org/alfresco/repo/node/ReferenceableAspect.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.copy.CopyServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Registers and contains the behaviour specific to the + * {@link org.alfresco.model.ContentModel#ASPECT_REFERENCEABLE referencable aspect}. + * + * @author Derek Hulley + */ +public class ReferenceableAspect implements CopyServicePolicies.OnCopyNodePolicy +{ + // Logger + private static final Log logger = LogFactory.getLog(ReferenceableAspect.class); + + // Dependencies + private PolicyComponent policyComponent; + + /** + * @param policyComponent the policy component to register behaviour with + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Initialise the Referencable Aspect + *

    + * Ensures that the {@link ContentModel#ASPECT_REFERENCEABLE referencable aspect} + * copy behaviour is disabled. + */ + public void init() + { + // disable copy for referencable aspect + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + ContentModel.ASPECT_REFERENCEABLE, + new JavaBehaviour(this, "onCopyNode")); + } + + /** + * Does nothing + */ + public void onCopyNode( + QName classRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + // don't copy + } +} diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java new file mode 100644 index 0000000000..b911f81d91 --- /dev/null +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -0,0 +1,1275 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.db; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.NodeAssoc; +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.repo.domain.PropertyValue; +import org.alfresco.repo.domain.Store; +import org.alfresco.repo.node.AbstractNodeServiceImpl; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidAspectException; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.AssociationExistsException; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.CyclicChildRelationshipException; +import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.InvalidStoreRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreExistsException; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.NodeRef.Status; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.springframework.util.Assert; + +/** + * Node service using database persistence layer to fulfill functionality + * + * @author Derek Hulley + */ +public class DbNodeServiceImpl extends AbstractNodeServiceImpl +{ + private final DictionaryService dictionaryService; + private final NodeDaoService nodeDaoService; + + public DbNodeServiceImpl( + PolicyComponent policyComponent, + DictionaryService dictionaryService, + NodeDaoService nodeDaoService) + { + super(policyComponent); + + this.dictionaryService = dictionaryService; + this.nodeDaoService = nodeDaoService; + } + + /** + * Performs a null-safe get of the node + * + * @param nodeRef the node to retrieve + * @return Returns the node entity (never null) + * @throws InvalidNodeRefException if the referenced node could not be found + */ + private Node getNodeNotNull(NodeRef nodeRef) throws InvalidNodeRefException + { + String protocol = nodeRef.getStoreRef().getProtocol(); + String identifier = nodeRef.getStoreRef().getIdentifier(); + Node unchecked = nodeDaoService.getNode(protocol, identifier, nodeRef.getId()); + if (unchecked == null) + { + throw new InvalidNodeRefException("Node does not exist: " + nodeRef, nodeRef); + } + return unchecked; + } + + public boolean exists(StoreRef storeRef) + { + Store store = nodeDaoService.getStore(storeRef.getProtocol(), storeRef.getIdentifier()); + boolean exists = (store != null); + // done + return exists; + } + + public boolean exists(NodeRef nodeRef) + { + StoreRef storeRef = nodeRef.getStoreRef(); + Node node = nodeDaoService.getNode(storeRef.getProtocol(), + storeRef.getIdentifier(), + nodeRef.getId()); + boolean exists = (node != null); + // done + return exists; + } + + public Status getNodeStatus(NodeRef nodeRef) + { + NodeStatus nodeStatus = nodeDaoService.getNodeStatus( + nodeRef.getStoreRef().getProtocol(), + nodeRef.getStoreRef().getIdentifier(), + nodeRef.getId()); + if (nodeStatus == null) // node never existed + { + return null; + } + else + { + return new NodeRef.Status( + nodeStatus.getChangeTxnId(), + nodeStatus.isDeleted()); + } + } + + /** + * @see NodeDaoService#getStores() + */ + public List getStores() + { + List stores = nodeDaoService.getStores(); + List storeRefs = new ArrayList(stores.size()); + for (Store store : stores) + { + storeRefs.add(store.getStoreRef()); + } + // done + return storeRefs; + } + + /** + * Defers to the typed service + * @see StoreDaoService#createWorkspace(String) + */ + public StoreRef createStore(String protocol, String identifier) + { + StoreRef storeRef = new StoreRef(protocol, identifier); + // check that the store does not already exist + Store store = nodeDaoService.getStore(protocol, identifier); + if (store != null) + { + throw new StoreExistsException("Unable to create a store that already exists", + new StoreRef(protocol, identifier)); + } + + // invoke policies + invokeBeforeCreateStore(ContentModel.TYPE_STOREROOT, storeRef); + + // create a new one + store = nodeDaoService.createStore(protocol, identifier); + // get the root node + Node rootNode = store.getRootNode(); + // assign the root aspect - this is expected of all roots, even store roots + addAspect(rootNode.getNodeRef(), + ContentModel.ASPECT_ROOT, + Collections.emptyMap()); + + // invoke policies + invokeOnCreateStore(rootNode.getNodeRef()); + + // done + if (!store.getStoreRef().equals(storeRef)) + { + throw new RuntimeException("Incorrect store reference"); + } + return storeRef; + } + + public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException + { + Store store = nodeDaoService.getStore(storeRef.getProtocol(), storeRef.getIdentifier()); + if (store == null) + { + throw new InvalidStoreRefException("Store does not exist", storeRef); + } + // get the root + Node node = store.getRootNode(); + if (node == null) + { + throw new InvalidStoreRefException("Store does not have a root node", storeRef); + } + NodeRef nodeRef = node.getNodeRef(); + // done + return nodeRef; + } + + /** + * @see #createNode(NodeRef, QName, QName, QName, Map) + */ + public ChildAssociationRef createNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName) + { + return this.createNode(parentRef, assocTypeQName, assocQName, nodeTypeQName, null); + } + + /** + * @see org.alfresco.service.cmr.repository.NodeService#createNode(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, java.util.Map) + */ + public ChildAssociationRef createNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName, + Map properties) + { + Assert.notNull(parentRef); + Assert.notNull(assocTypeQName); + Assert.notNull(assocQName); + + // null property map is allowed + if (properties == null) + { + properties = new HashMap(); + } + else + { + // Copy the incomming property map since we may need to modify it later + properties = new HashMap(properties); + } + + // Invoke policy behaviour + invokeBeforeUpdateNode(parentRef); + invokeBeforeCreateNode(parentRef, assocTypeQName, assocQName, nodeTypeQName); + + // get the store that the parent belongs to + StoreRef storeRef = parentRef.getStoreRef(); + Store store = nodeDaoService.getStore(storeRef.getProtocol(), storeRef.getIdentifier()); + if (store == null) + { + throw new RuntimeException("No store found for parent node: " + parentRef); + } + + // check the node type + TypeDefinition nodeTypeDef = dictionaryService.getType(nodeTypeQName); + if (nodeTypeDef == null) + { + throw new InvalidTypeException(nodeTypeQName); + } + + // get/generate an ID for the node + String newId = generateGuid(properties); + + // create the node instance + Node node = nodeDaoService.newNode(store, newId, nodeTypeQName); + + // get the parent node + Node parentNode = getNodeNotNull(parentRef); + + // create the association - invoke policy behaviour + ChildAssoc childAssoc = nodeDaoService.newChildAssoc(parentNode, node, true, assocTypeQName, assocQName); + ChildAssociationRef childAssocRef = childAssoc.getChildAssocRef(); + + // Set the default property values + addDefaultPropertyValues(nodeTypeDef, properties); + + // Add the default aspects to the node + addDefaultAspects(nodeTypeDef, node, childAssocRef.getChildRef(), properties); + + // set the properties - it is a new node so only set properties if there are any + if (properties.size() > 0) + { + this.setProperties(node.getNodeRef(), properties); + } + + // Invoke policy behaviour + invokeOnCreateNode(childAssocRef); + invokeOnUpdateNode(parentRef); + + // done + return childAssocRef; + } + + /** + * Add the default aspects to a given node + * + * @param nodeTypeDef + */ + private void addDefaultAspects(ClassDefinition classDefinition, Node node, NodeRef nodeRef, Map properties) + { + // get the mandatory aspects for the node type + List defaultAspectDefs = classDefinition.getDefaultAspects(); + + // add all the aspects to the node + Set nodeAspects = node.getAspects(); + for (AspectDefinition defaultAspectDef : defaultAspectDefs) + { + invokeBeforeAddAspect(nodeRef, defaultAspectDef.getName()); + nodeAspects.add(defaultAspectDef.getName()); + addDefaultPropertyValues(defaultAspectDef, properties); + invokeOnAddAspect(nodeRef, defaultAspectDef.getName()); + + // Now add any default aspects for this aspect + addDefaultAspects(defaultAspectDef, node, nodeRef, properties); + } + } + + /** + * Sets the default property values + * + * @param classDefinition + * @param properties + */ + private void addDefaultPropertyValues(ClassDefinition classDefinition, Map properties) + { + for (Map.Entry entry : classDefinition.getDefaultValues().entrySet()) + { + if (properties.containsKey(entry.getKey()) == false) + { + // Set the default value of the property + properties.put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Drops the old primary association and creates a new one + */ + public ChildAssociationRef moveNode( + NodeRef nodeToMoveRef, + NodeRef newParentRef, + QName assocTypeQName, + QName assocQName) + throws InvalidNodeRefException + { + Assert.notNull(nodeToMoveRef); + Assert.notNull(newParentRef); + Assert.notNull(assocTypeQName); + Assert.notNull(assocQName); + + // check the node references + Node nodeToMove = getNodeNotNull(nodeToMoveRef); + Node newParentNode = getNodeNotNull(newParentRef); + // get the primary parent assoc + ChildAssoc oldAssoc = nodeDaoService.getPrimaryParentAssoc(nodeToMove); + ChildAssociationRef oldAssocRef = oldAssoc.getChildAssocRef(); + // get the old parent + Node oldParentNode = oldAssoc.getParent(); + + // Invoke policy behaviour + invokeBeforeDeleteChildAssociation(oldAssocRef); + invokeBeforeCreateChildAssociation(newParentRef, nodeToMoveRef, assocTypeQName, assocQName); + invokeBeforeUpdateNode(oldParentNode.getNodeRef()); // old parent will be updated + invokeBeforeUpdateNode(newParentRef); // new parent ditto + + // remove the child assoc from the old parent + // don't cascade as we will still need the node afterwards + nodeDaoService.deleteChildAssoc(oldAssoc, false); + // create a new assoc + ChildAssoc newAssoc = nodeDaoService.newChildAssoc(newParentNode, nodeToMove, true, assocTypeQName, assocQName); + + // check that no cyclic relationships have been created + getPaths(nodeToMoveRef, false); + + // invoke policy behaviour + invokeOnCreateChildAssociation(newAssoc.getChildAssocRef()); + invokeOnDeleteChildAssociation(oldAssoc.getChildAssocRef()); + invokeOnUpdateNode(oldParentNode.getNodeRef()); + invokeOnUpdateNode(newParentRef); + + // update the node status + NodeStatus nodeStatus = nodeToMove.getStatus(); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + + // done + return newAssoc.getChildAssocRef(); + } + + public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index) + { + // get nodes + Node parentNode = getNodeNotNull(childAssocRef.getParentRef()); + Node childNode = getNodeNotNull(childAssocRef.getChildRef()); + + ChildAssoc assoc = nodeDaoService.getChildAssoc( + parentNode, + childNode, + childAssocRef.getTypeQName(), + childAssocRef.getQName()); + if (assoc == null) + { + throw new InvalidChildAssociationRefException("Unable to set child association index: \n" + + " assoc: " + childAssocRef + "\n" + + " index: " + index, + childAssocRef); + } + // set the index + assoc.setIndex(index); + } + + public QName getType(NodeRef nodeRef) throws InvalidNodeRefException + { + Node node = getNodeNotNull(nodeRef); + return node.getTypeQName(); + } + + /** + * @see org.alfresco.service.cmr.repository.NodeService#setType(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException + { + // check the node type + TypeDefinition nodeTypeDef = dictionaryService.getType(typeQName); + if (nodeTypeDef == null) + { + throw new InvalidTypeException(typeQName); + } + + // Invoke policies + invokeBeforeUpdateNode(nodeRef); + + // Get the node and set the new type + Node node = getNodeNotNull(nodeRef); + node.setTypeQName(typeQName); + + // Add the default aspects to the node (update the properties with any new default values) + Map properties = this.getProperties(nodeRef); + addDefaultAspects(nodeTypeDef, node, nodeRef, properties); + this.setProperties(nodeRef, properties); + + // Invoke policies + invokeOnUpdateNode(nodeRef); + } + + /** + * @see Node#getAspects() + */ + public void addAspect( + NodeRef nodeRef, + QName aspectTypeQName, + Map aspectProperties) + throws InvalidNodeRefException, InvalidAspectException + { + // check that the aspect is legal + AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName); + if (aspectDef == null) + { + throw new InvalidAspectException("The aspect is invalid: " + aspectTypeQName, aspectTypeQName); + } + + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + invokeBeforeAddAspect(nodeRef, aspectTypeQName); + + Node node = getNodeNotNull(nodeRef); + + // attach the properties to the current node properties + Map nodeProperties = getProperties(nodeRef); + + if (aspectProperties != null) + { + nodeProperties.putAll(aspectProperties); + } + + // Set any default property values that appear on the aspect + addDefaultPropertyValues(aspectDef, nodeProperties); + + // Add any dependant aspect + addDefaultAspects(aspectDef, node, nodeRef, nodeProperties); + + // Set the property values back on the node + setProperties(nodeRef, nodeProperties); + + // physically attach the aspect to the node + if (node.getAspects().add(aspectTypeQName) == true) + { + // Invoke policy behaviours + invokeOnUpdateNode(nodeRef); + invokeOnAddAspect(nodeRef, aspectTypeQName); + + // update the node status + NodeStatus nodeStatus = node.getStatus(); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + } + } + + /** + * @see Node#getAspects() + */ + public void removeAspect(NodeRef nodeRef, QName aspectTypeQName) + throws InvalidNodeRefException, InvalidAspectException + { + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + invokeBeforeRemoveAspect(nodeRef, aspectTypeQName); + + // get the aspect + AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName); + if (aspectDef == null) + { + throw new InvalidAspectException(aspectTypeQName); + } + // get the node + Node node = getNodeNotNull(nodeRef); + + // check that the aspect may be removed + TypeDefinition nodeTypeDef = dictionaryService.getType(node.getTypeQName()); + if (nodeTypeDef == null) + { + throw new InvalidNodeRefException("The node type is no longer valid: " + nodeRef, nodeRef); + } + List defaultAspects = nodeTypeDef.getDefaultAspects(); + if (defaultAspects.contains(aspectDef)) + { + throw new InvalidAspectException( + "The aspect is a default for the node's type and cannot be removed: " + aspectTypeQName, + aspectTypeQName); + } + + // remove the aspect, if present + boolean removed = node.getAspects().remove(aspectTypeQName); + // if the aspect was present, remove the associated properties + if (removed) + { + Map nodeProperties = node.getProperties(); + Map propertyDefs = aspectDef.getProperties(); + for (QName propertyName : propertyDefs.keySet()) + { + nodeProperties.remove(propertyName); + } + + // Invoke policy behaviours + invokeOnUpdateNode(nodeRef); + invokeOnRemoveAspect(nodeRef, aspectTypeQName); + + // update the node status + NodeStatus nodeStatus = node.getStatus(); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + } + } + + /** + * Performs a check on the set of node aspects + * + * @see Node#getAspects() + */ + public boolean hasAspect(NodeRef nodeRef, QName aspectRef) throws InvalidNodeRefException, InvalidAspectException + { + Node node = getNodeNotNull(nodeRef); + Set aspectQNames = node.getAspects(); + boolean hasAspect = aspectQNames.contains(aspectRef); + // done + return hasAspect; + } + + public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException + { + Node node = getNodeNotNull(nodeRef); + Set aspectQNames = node.getAspects(); + // copy the set to ensure initialization + Set ret = new HashSet(aspectQNames.size()); + ret.addAll(aspectQNames); + // done + return ret; + } + + public void deleteNode(NodeRef nodeRef) + { + // Invoke policy behaviours + invokeBeforeDeleteNode(nodeRef); + + // get the node + Node node = getNodeNotNull(nodeRef); + // get the primary parent-child relationship before it is gone + ChildAssociationRef childAssocRef = getPrimaryParent(nodeRef); + // get type and aspect QNames as they will be unavailable after the delete + QName nodeTypeQName = node.getTypeQName(); + Set nodeAspectQNames = node.getAspects(); + // delete it + nodeDaoService.deleteNode(node, true); + + // Invoke policy behaviours + invokeOnDeleteNode(childAssocRef, nodeTypeQName, nodeAspectQNames); + } +// /** +// * Recursive method to ensure cascade-deletion works with full invocation of policy behaviours. +// *

    +// * The recursion will first cascade down primary associations before deleting all regular and +// * child associations to and from it. After this, the node itself is deleted. This bottom-up +// * behaviour ensures that the policy invocation behaviour, which currently relies on being able +// * to inspect association source types, gets fired correctly. +// */ +// public void deleteNode(NodeRef nodeRef) +// { +// // Invoke policy behaviours +// invokeBeforeDeleteNode(nodeRef); +// +// // get the node +// Node node = getNodeNotNull(nodeRef); +// +// // get node info (for invocation purposes) before any deletions occur +// // get the primary parent-child relationship before it is gone +// ChildAssociationRef primaryParentAssocRef = getPrimaryParent(nodeRef); +// // get type and aspect QNames as they will be unavailable after the delete +// QName nodeTypeQName = node.getTypeQName(); +// Set nodeAspectQNames = node.getAspects(); +// +// // get all associations, forcing a load of the collections +// Collection childAssocs = new ArrayList(node.getChildAssocs()); +// Collection parentAssocs = new ArrayList(node.getParentAssocs()); +// Collection sourceAssocs = new ArrayList(node.getSourceNodeAssocs()); +// Collection targetAssocs = new ArrayList(node.getTargetNodeAssocs()); +// +// // remove all child associations, including the primary one +// for (ChildAssoc childAssoc : childAssocs) +// { +// ChildAssociationRef childAssocRef = childAssoc.getChildAssocRef(); +// // cascade into primary associations +// if (childAssoc.getIsPrimary()) +// { +// NodeRef childNodeRef = childAssocRef.getChildRef(); +// this.deleteNode(childNodeRef); +// } +// +// // one or more of these associations may have been dealt with when deleting the +// // child, so check that the association is valid +// +// // invoke pre-deletion behaviour +// invokeBeforeDeleteChildAssociation(childAssocRef); +// // remove it - cascade just to be sure +// nodeDaoService.deleteChildAssoc(childAssoc, true); +// // invoke post-deletion behaviour +// invokeOnDeleteChildAssociation(childAssocRef); +// } +// // remove all parent associations, including the primary one +// for (ChildAssoc parentAssoc : parentAssocs) +// { +// ChildAssociationRef parentAssocRef = parentAssoc.getChildAssocRef(); +// // invoke pre-deletion behaviour +// invokeBeforeDeleteChildAssociation(parentAssocRef); +// // remove it - don't cascade as this is a parent assoc +// nodeDaoService.deleteChildAssoc(parentAssoc, false); +// // invoke post-deletion behaviour +// invokeOnDeleteChildAssociation(parentAssocRef); +// } +// // remove all source node associations +// for (NodeAssoc sourceAssoc : sourceAssocs) +// { +// AssociationRef sourceAssocRef = sourceAssoc.getNodeAssocRef(); +// // remove it +// nodeDaoService.deleteNodeAssoc(sourceAssoc); +// // invoke post-deletion behaviour +// invokeOnDeleteAssociation(sourceAssocRef); +// } +// // remove all target node associations +// for (NodeAssoc targetAssoc : targetAssocs) +// { +// AssociationRef targetAssocRef = targetAssoc.getNodeAssocRef(); +// // remove it +// nodeDaoService.deleteNodeAssoc(targetAssoc); +// // invoke post-deletion behaviour +// invokeOnDeleteAssociation(targetAssocRef); +// } +// +// // delete it +// // We cascade so that we are sure that any new children created by policy implementations are +// // removed. There won't be any noticiations for these, but it prevents the cascade and +// // notifications from chasing each other +// nodeDaoService.deleteNode(node, true); +// +// // Invoke policy behaviours +// invokeOnDeleteNode(primaryParentAssocRef, nodeTypeQName, nodeAspectQNames); +// } + + public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName) + { + // Invoke policy behaviours + invokeBeforeUpdateNode(parentRef); + invokeBeforeCreateChildAssociation(parentRef, childRef, assocTypeQName, assocQName); + + // check that both nodes belong to the same store + if (!parentRef.getStoreRef().equals(childRef.getStoreRef())) + { + throw new InvalidNodeRefException("Parent and child nodes must belong to the same store: \n" + + " parent: " + parentRef + "\n" + + " child: " + childRef, + childRef); + } + + // get the parent node and ensure that it is a container node + Node parentNode = getNodeNotNull(parentRef); + // get the child node + Node childNode = getNodeNotNull(childRef); + // make the association + ChildAssoc assoc = nodeDaoService.newChildAssoc(parentNode, childNode, false, assocTypeQName, assocQName); + ChildAssociationRef assocRef = assoc.getChildAssocRef(); + NodeRef childNodeRef = assocRef.getChildRef(); + + // check that the child addition of the child has not created a cyclic relationship + // this functionality is provided for free in getPath + getPaths(childNodeRef, false); + + // Invoke policy behaviours + invokeOnCreateChildAssociation(assocRef); + invokeOnUpdateNode(parentRef); + + return assoc.getChildAssocRef(); + } + + public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException + { + Node parentNode = getNodeNotNull(parentRef); + Node childNode = getNodeNotNull(childRef); + NodeKey childNodeKey = childNode.getKey(); + + // get all the child assocs + ChildAssociationRef primaryAssocRef = null; + Collection assocs = parentNode.getChildAssocs(); + assocs = new HashSet(assocs); // copy set as we will be modifying it + for (ChildAssoc assoc : assocs) + { + if (!assoc.getChild().getKey().equals(childNodeKey)) + { + continue; // not a matching association + } + ChildAssociationRef assocRef = assoc.getChildAssocRef(); + // Is this a primary association? + if (assoc.getIsPrimary()) + { + // keep the primary associaton for last + primaryAssocRef = assocRef; + } + else + { + // delete the association instance - it is not primary + invokeBeforeDeleteChildAssociation(assocRef); + nodeDaoService.deleteChildAssoc(assoc, true); // cascade + invokeOnDeleteChildAssociation(assocRef); + } + } + // remove the child if the primary association was a match + if (primaryAssocRef != null) + { + deleteNode(primaryAssocRef.getChildRef()); + } + + // Invoke policy behaviours + invokeOnUpdateNode(parentRef); + + // done + } + + public Map getProperties(NodeRef nodeRef) throws InvalidNodeRefException + { + Node node = getNodeNotNull(nodeRef); + Map nodeProperties = node.getProperties(); + Map ret = new HashMap(nodeProperties.size()); + // copy values + for (Map.Entry entry: nodeProperties.entrySet()) + { + QName propertyQName = entry.getKey(); + PropertyValue propertyValue = entry.getValue(); + // get the property definition + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + // convert to the correct type + Serializable value = makeSerializableValue(propertyDef, propertyValue); + // copy across + ret.put(propertyQName, value); + } + // spoof referencable properties + addReferencableProperties(nodeRef, ret); + // done + return ret; + } + + public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException + { + // spoof referencable properties + if (qname.equals(ContentModel.PROP_STORE_PROTOCOL)) + { + return nodeRef.getStoreRef().getProtocol(); + } + else if (qname.equals(ContentModel.PROP_STORE_IDENTIFIER)) + { + return nodeRef.getStoreRef().getIdentifier(); + } + else if (qname.equals(ContentModel.PROP_NODE_UUID)) + { + return nodeRef.getId(); + } + + // get the property from the node + Node node = getNodeNotNull(nodeRef); + Map properties = node.getProperties(); + PropertyValue propertyValue = properties.get(qname); + + // get the property definition + PropertyDefinition propertyDef = dictionaryService.getProperty(qname); + // convert to the correct type + Serializable value = makeSerializableValue(propertyDef, propertyValue); + // done + return value; + } + + /** + * Ensures that all required properties are present on the node and copies the + * property values to the Node. + *

    + * To remove a property, remove it from the map before calling this method. + * Null-valued properties are allowed. + *

    + * If any of the values are null, a marker object is put in to mimic nulls. They will be turned back into + * a real nulls when the properties are requested again. + * + * @see Node#getProperties() + */ + public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException + { + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + + if (properties == null) + { + throw new IllegalArgumentException("Properties may not be null"); + } + + // remove referencable properties + removeReferencableProperties(properties); + + // find the node + Node node = getNodeNotNull(nodeRef); + // get the properties before + Map propertiesBefore = getProperties(nodeRef); + + // copy properties onto node + Map nodeProperties = node.getProperties(); + nodeProperties.clear(); + + // check the property type and copy the values across + for (QName propertyQName : properties.keySet()) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + Serializable value = properties.get(propertyQName); + // get a persistable value + PropertyValue propertyValue = makePropertyValue(propertyDef, value); + nodeProperties.put(propertyQName, propertyValue); + } + + // store the properties after the change + Map propertiesAfter = Collections.unmodifiableMap(properties); + + // Invoke policy behaviours + invokeOnUpdateNode(nodeRef); + invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter); + + // update the node status + NodeStatus nodeStatus = node.getStatus(); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + } + + /** + * Gets the properties map, sets the value (null is allowed) and checks that the new set + * of properties is valid. + * + * @see DbNodeServiceImpl.NullPropertyValue + */ + public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException + { + Assert.notNull(qname); + + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + + // get the node + Node node = getNodeNotNull(nodeRef); + // get properties before + Map propertiesBefore = getProperties(nodeRef); + + Map properties = node.getProperties(); + PropertyDefinition propertyDef = dictionaryService.getProperty(qname); + // get a persistable value + PropertyValue propertyValue = makePropertyValue(propertyDef, value); + properties.put(qname, propertyValue); + + // get properties after the change + Map propertiesAfter = getProperties(nodeRef); + + // Invoke policy behaviours + invokeOnUpdateNode(nodeRef); + invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter); + + // update the node status + NodeStatus nodeStatus = node.getStatus(); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + } + + /** + * Transforms {@link Node#getParentAssocs()} to a new collection + */ + public Collection getParents(NodeRef nodeRef) throws InvalidNodeRefException + { + Node node = getNodeNotNull(nodeRef); + // get the assocs pointing to it + Collection parentAssocs = node.getParentAssocs(); + // list of results + Collection results = new ArrayList(parentAssocs.size()); + for (ChildAssoc assoc : parentAssocs) + { + // get the parent + Node parentNode = assoc.getParent(); + results.add(parentNode.getNodeRef()); + } + // done + return results; + } + + /** + * Filters out any associations if their qname is not a match to the given pattern. + */ + public List getParentAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern, QNamePattern qnamePattern) + { + Node node = getNodeNotNull(nodeRef); + // get the assocs pointing to it + Collection parentAssocs = node.getParentAssocs(); + // shortcut if there are no assocs + if (parentAssocs.size() == 0) + { + return Collections.emptyList(); + } + // list of results + List results = new ArrayList(parentAssocs.size()); + for (ChildAssoc assoc : parentAssocs) + { + // does the qname match the pattern? + if (!qnamePattern.isMatch(assoc.getQname()) || !typeQNamePattern.isMatch(assoc.getTypeQName())) + { + // no match - ignore + continue; + } + results.add(assoc.getChildAssocRef()); + } + // done + return results; + } + + /** + * Filters out any associations if their qname is not a match to the given pattern. + */ + public List getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern, QNamePattern qnamePattern) + { + Node node = getNodeNotNull(nodeRef); + // get the assocs pointing from it + Collection childAssocs = node.getChildAssocs(); + // shortcut if there are no assocs + if (childAssocs.size() == 0) + { + return Collections.emptyList(); + } + // sort results + ArrayList orderedList = new ArrayList(childAssocs); + Collections.sort(orderedList); + + // list of results + List results = new ArrayList(childAssocs.size()); + int nthSibling = 0; + for (ChildAssoc assoc : orderedList) + { + // does the qname match the pattern? + if (!qnamePattern.isMatch(assoc.getQname()) || !typeQNamePattern.isMatch(assoc.getTypeQName())) + { + // no match - ignore + continue; + } + ChildAssociationRef assocRef = assoc.getChildAssocRef(); + // slot the value in the right spot + assocRef.setNthSibling(nthSibling); + nthSibling++; + // get the child + results.add(assoc.getChildAssocRef()); + } + // done + return results; + } + + public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException + { + Node node = getNodeNotNull(nodeRef); + // get the primary parent assoc + ChildAssoc assoc = nodeDaoService.getPrimaryParentAssoc(node); + + // done - the assoc may be null for a root node + ChildAssociationRef assocRef = null; + if (assoc == null) + { + assocRef = new ChildAssociationRef(null, null, null, nodeRef); + } + else + { + assocRef = assoc.getChildAssocRef(); + } + return assocRef; + } + + public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) + throws InvalidNodeRefException, AssociationExistsException + { + // Invoke policy behaviours + invokeBeforeUpdateNode(sourceRef); + + Node sourceNode = getNodeNotNull(sourceRef); + Node targetNode = getNodeNotNull(targetRef); + // see if it exists + NodeAssoc assoc = nodeDaoService.getNodeAssoc(sourceNode, targetNode, assocTypeQName); + if (assoc != null) + { + throw new AssociationExistsException(sourceRef, targetRef, assocTypeQName); + } + // we are sure that the association doesn't exist - make it + assoc = nodeDaoService.newNodeAssoc(sourceNode, targetNode, assocTypeQName); + AssociationRef assocRef = assoc.getNodeAssocRef(); + + // Invoke policy behaviours + invokeOnUpdateNode(sourceRef); + invokeOnCreateAssociation(assocRef); + + return assocRef; + } + + public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) + throws InvalidNodeRefException + { + // Invoke policy behaviours + invokeBeforeUpdateNode(sourceRef); + + Node sourceNode = getNodeNotNull(sourceRef); + Node targetNode = getNodeNotNull(targetRef); + // get the association + NodeAssoc assoc = nodeDaoService.getNodeAssoc(sourceNode, targetNode, assocTypeQName); + AssociationRef assocRef = assoc.getNodeAssocRef(); + // delete it + nodeDaoService.deleteNodeAssoc(assoc); + + // Invoke policy behaviours + invokeOnUpdateNode(sourceRef); + invokeOnDeleteAssociation(assocRef); + } + + public List getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern) + throws InvalidNodeRefException + { + Node sourceNode = getNodeNotNull(sourceRef); + // get all assocs to target + Collection assocs = sourceNode.getTargetNodeAssocs(); + List nodeAssocRefs = new ArrayList(assocs.size()); + for (NodeAssoc assoc : assocs) + { + // check qname pattern + if (!qnamePattern.isMatch(assoc.getTypeQName())) + { + continue; // the assoc name doesn't match the pattern given + } + nodeAssocRefs.add(assoc.getNodeAssocRef()); + } + // done + return nodeAssocRefs; + } + + public List getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern) + throws InvalidNodeRefException + { + Node sourceNode = getNodeNotNull(targetRef); + // get all assocs to source + Collection assocs = sourceNode.getSourceNodeAssocs(); + List nodeAssocRefs = new ArrayList(assocs.size()); + for (NodeAssoc assoc : assocs) + { + // check qname pattern + if (!qnamePattern.isMatch(assoc.getTypeQName())) + { + continue; // the assoc name doesn't match the pattern given + } + nodeAssocRefs.add(assoc.getNodeAssocRef()); + } + // done + return nodeAssocRefs; + } + + /** + * Recursive method used to build up paths from a given node to the root. + *

    + * Whilst walking up the hierarchy to the root, some nodes may have a root aspect. + * Everytime one of these is encountered, a new path is farmed off, but the method + * continues to walk up the hierarchy. + * + * @param currentNode the node to start from, i.e. the child node to work upwards from + * @param currentPath the path from the current node to the descendent that we started from + * @param completedPaths paths that have reached the root are added to this collection + * @param assocStack the parent-child relationships traversed whilst building the path. + * Used to detected cyclic relationships. + * @param primaryOnly true if only the primary parent association must be traversed. + * If this is true, then the only root is the top level node having no parents. + * @throws CyclicChildRelationshipException + */ + private void prependPaths( + final Node currentNode, + final Path currentPath, + Collection completedPaths, + Stack assocStack, + boolean primaryOnly) + throws CyclicChildRelationshipException + { + NodeRef currentNodeRef = currentNode.getNodeRef(); + // get the parent associations of the given node + Collection parentAssocs = currentNode.getParentAssocs(); + // does the node have parents + boolean hasParents = parentAssocs.size() > 0; + // does the current node have a root aspect? + boolean isRoot = hasAspect(currentNodeRef, ContentModel.ASPECT_ROOT); + boolean isStoreRoot = currentNode.getTypeQName().equals(ContentModel.TYPE_STOREROOT); + + // look for a root. If we only want the primary root, then ignore all but the top-level root. + if (isRoot && !(primaryOnly && hasParents)) // exclude primary search with parents present + { + // create a one-sided assoc ref for the root node and prepend to the stack + // this effectively spoofs the fact that the current node is not below the root + // - we put this assoc in as the first assoc in the path must be a one-sided + // reference pointing to the root node + ChildAssociationRef assocRef = new ChildAssociationRef( + null, + null, + null, + getRootNode(currentNode.getNodeRef().getStoreRef())); + // create a path to save and add the 'root' assoc + Path pathToSave = new Path(); + Path.ChildAssocElement first = null; + for (Path.Element element: currentPath) + { + if (first == null) + { + first = (Path.ChildAssocElement) element; + } + else + { + pathToSave.append(element); + } + } + if (first != null) + { + // mimic an association that would appear if the current node was below + // the root node + // or if first beneath the root node it will make the real thing + ChildAssociationRef updateAssocRef = new ChildAssociationRef( + isStoreRoot ? ContentModel.ASSOC_CHILDREN : first.getRef().getTypeQName(), + getRootNode(currentNode.getNodeRef().getStoreRef()), + first.getRef().getQName(), + first.getRef().getChildRef()); + Path.Element newFirst = new Path.ChildAssocElement(updateAssocRef); + pathToSave.prepend(newFirst); + } + + Path.Element element = new Path.ChildAssocElement(assocRef); + pathToSave.prepend(element); + + // store the path just built + completedPaths.add(pathToSave); + } + + if (parentAssocs.size() == 0 && !isRoot) + { + throw new RuntimeException("Node without parents does not have root aspect: " + + currentNodeRef); + } + // walk up each parent association + for (ChildAssoc assoc : parentAssocs) + { + // does the association already exist in the stack + if (assocStack.contains(assoc)) + { + // the association was present already + throw new CyclicChildRelationshipException( + "Cyclic parent-child relationship detected: \n" + + " current node: " + currentNode + "\n" + + " current path: " + currentPath + "\n" + + " next assoc: " + assoc, + assoc); + } + // do we consider only primary assocs? + if (primaryOnly && !assoc.getIsPrimary()) + { + continue; + } + // build a path element + NodeRef parentRef = assoc.getParent().getNodeRef(); + QName qname = assoc.getQname(); + NodeRef childRef = assoc.getChild().getNodeRef(); + boolean isPrimary = assoc.getIsPrimary(); + // build a real association reference + ChildAssociationRef assocRef = new ChildAssociationRef(assoc.getTypeQName(), parentRef, qname, childRef, isPrimary, -1); + // Ordering is not important here: We are building distinct paths upwards + Path.Element element = new Path.ChildAssocElement(assocRef); + // create a new path that builds on the current path + Path path = new Path(); + path.append(currentPath); + // prepend element + path.prepend(element); + // get parent node + Node parentNode = assoc.getParent(); + + // push the assoc stack, recurse and pop + assocStack.push(assoc); + prependPaths(parentNode, path, completedPaths, assocStack, primaryOnly); + assocStack.pop(); + } + // done + } + + /** + * @see #getPaths(NodeRef, boolean) + * @see #prependPaths(Node, Path, Collection, Stack, boolean) + */ + public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException + { + List paths = getPaths(nodeRef, true); // checks primary path count + if (paths.size() == 1) + { + return paths.get(0); // we know there is only one + } + throw new RuntimeException("Primary path count not checked"); // checked by getPaths() + } + + /** + * When searching for primaryOnly == true, checks that there is exactly + * one path. + * @see #prependPaths(Node, Path, Collection, Stack, boolean) + */ + public List getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException + { + // get the starting node + Node node = getNodeNotNull(nodeRef); + // create storage for the paths - only need 1 bucket if we are looking for the primary path + List paths = new ArrayList(primaryOnly ? 1 : 10); + // create an empty current path to start from + Path currentPath = new Path(); + // create storage for touched associations + Stack assocStack = new Stack(); + // call recursive method to sort it out + prependPaths(node, currentPath, paths, assocStack, primaryOnly); + + // check that for the primary only case we have exactly one path + if (primaryOnly && paths.size() != 1) + { + throw new RuntimeException("Node has " + paths.size() + " primary paths: " + nodeRef); + } + + // done + return paths; + } +} diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImplTest.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImplTest.java new file mode 100644 index 0000000000..78fe904c47 --- /dev/null +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImplTest.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.db; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; + +/** + * @see org.alfresco.repo.node.db.DbNodeServiceImpl + * + * @author Derek Hulley + */ +public class DbNodeServiceImplTest extends BaseNodeServiceTest +{ + private TransactionService txnService; + private NodeDaoService nodeDaoService; + + protected NodeService getNodeService() + { + return (NodeService) applicationContext.getBean("NodeService"); + } + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + txnService = (TransactionService) applicationContext.getBean("transactionComponent"); + nodeDaoService = (NodeDaoService) applicationContext.getBean("nodeDaoService"); + } + + /** + * Deletes a child node and then iterates over the children of the parent node, + * getting the QName. This caused some issues after we did some optimization + * using lazy loading of the associations. + */ + public void testLazyLoadIssue() throws Exception + { + Map assocRefs = buildNodeGraph(); + // commit results + setComplete(); + endTransaction(); + + UserTransaction userTransaction = txnService.getUserTransaction(); + + try + { + userTransaction.begin(); + + ChildAssociationRef n6pn8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")); + NodeRef n6Ref = n6pn8Ref.getParentRef(); + NodeRef n8Ref = n6pn8Ref.getChildRef(); + + // delete n8 + nodeService.deleteNode(n8Ref); + + // get the parent children + List assocs = nodeService.getChildAssocs(n6Ref); + for (ChildAssociationRef assoc : assocs) + { + // just checking + } + + userTransaction.commit(); + } + catch(Exception e) + { + try { userTransaction.rollback(); } catch (IllegalStateException ee) {} + throw e; + } + } + + /** + * Checks that the node status changes correctly during: + *

      + *
    • creation
    • + *
    • property changes
    • + *
    • aspect changes
    • + *
    • moving
    • + *
    • deletion
    • + *
    + */ + public void testNodeStatus() throws Exception + { + Map assocRefs = buildNodeGraph(); + // get the node to play with + ChildAssociationRef n6pn8Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n6_p_n8")); + final NodeRef n6Ref = n6pn8Ref.getParentRef(); + final NodeRef n8Ref = n6pn8Ref.getChildRef(); + final Map properties = nodeService.getProperties(n6Ref); + + // commit results + setComplete(); + endTransaction(); + + // change property - check status + TransactionWork changePropertiesWork = new TransactionWork() + { + public Object doWork() + { + nodeService.setProperty(n6Ref, ContentModel.PROP_CREATED, new Date()); + return null; + } + }; + executeAndCheck(n6Ref, changePropertiesWork); + + // add an aspect + TransactionWork addAspectWork = new TransactionWork() + { + public Object doWork() + { + nodeService.addAspect(n6Ref, ASPECT_QNAME_TEST_MARKER, null); + return null; + } + }; + executeAndCheck(n6Ref, addAspectWork); + + // remove an aspect + TransactionWork removeAspectWork = new TransactionWork() + { + public Object doWork() + { + nodeService.removeAspect(n6Ref, ASPECT_QNAME_TEST_MARKER); + return null; + } + }; + executeAndCheck(n6Ref, removeAspectWork); + + // move the node + TransactionWork moveNodeWork = new TransactionWork() + { + public Object doWork() + { + nodeService.moveNode( + n6Ref, + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(NAMESPACE, "moved")); + return null; + } + }; + executeAndCheck(n6Ref, moveNodeWork); + + // delete the node + TransactionWork deleteNodeWork = new TransactionWork() + { + public Object doWork() + { + nodeService.deleteNode(n6Ref); + return null; + } + }; + executeAndCheck(n6Ref, deleteNodeWork); + + // check cascade-deleted nodes + TransactionWork checkCascadeWork = new TransactionWork() + { + public Object doWork() + { + // check n6 + NodeStatus n6Status = nodeDaoService.getNodeStatus( + n6Ref.getStoreRef().getProtocol(), + n6Ref.getStoreRef().getIdentifier(), + n6Ref.getId()); + if (!n6Status.isDeleted()) + { + throw new RuntimeException("Deleted node does not have deleted status"); + } + // n8 is a primary child - it should be deleted too + NodeStatus n8Status = nodeDaoService.getNodeStatus( + n8Ref.getStoreRef().getProtocol(), + n8Ref.getStoreRef().getIdentifier(), + n8Ref.getId()); + if (!n8Status.isDeleted()) + { + throw new RuntimeException("Cascade-deleted node does not have deleted status"); + } + return null; + } + }; + TransactionUtil.executeInUserTransaction(txnService, checkCascadeWork); + + // check node recreation + TransactionWork checkRecreateWork = new TransactionWork() + { + public Object doWork() + { + properties.put(ContentModel.PROP_STORE_PROTOCOL, n6Ref.getStoreRef().getProtocol()); + properties.put(ContentModel.PROP_STORE_IDENTIFIER, n6Ref.getStoreRef().getIdentifier()); + properties.put(ContentModel.PROP_NODE_UUID, n6Ref.getId()); + + // recreate n6 + nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(NAMESPACE, "recreated-n6"), + ContentModel.TYPE_CONTAINER, + properties); + return null; + } + }; + TransactionUtil.executeInUserTransaction(txnService, checkRecreateWork); + } + + private void executeAndCheck(NodeRef nodeRef, TransactionWork work) throws Exception + { + UserTransaction txn = txnService.getUserTransaction(); + txn.begin(); + + NodeRef.Status currentStatus = nodeService.getNodeStatus(nodeRef); + assertNotNull(currentStatus); + String currentTxnId = AlfrescoTransactionSupport.getTransactionId(); + assertNotNull(currentTxnId); + assertNotSame(currentTxnId, currentStatus.getChangeTxnId()); + try + { + work.doWork(); + // get the status + NodeRef.Status newStatus = nodeService.getNodeStatus(nodeRef); + assertNotNull(newStatus); + // check + assertEquals("Change didn't update status", currentTxnId, newStatus.getChangeTxnId()); + txn.commit(); + } + catch (Exception e) + { + try { txn.rollback(); } catch (Throwable ee) {} + throw e; + } + } +} diff --git a/source/java/org/alfresco/repo/node/db/NodeDaoService.java b/source/java/org/alfresco/repo/node/db/NodeDaoService.java new file mode 100644 index 0000000000..c24c36b565 --- /dev/null +++ b/source/java/org/alfresco/repo/node/db/NodeDaoService.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.db; + +import java.util.Collection; +import java.util.List; + +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.NodeAssoc; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.repo.domain.Store; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.namespace.QName; + +/** + * Service layer accessing persistent node entities directly + * + * @author Derek Hulley + */ +public interface NodeDaoService +{ + /** + * Are there any pending changes which must be synchronized with the store? + * + * @return true => changes are pending + */ + public boolean isDirty(); + + /** + * Fetch a list of all stores in the repository + * + * @return Returns a list of stores + */ + public List getStores(); + + /** + * Creates a unique store for the given protocol and identifier combination + * + * @param protocol a protocol, e.g. {@link org.alfresco.service.cmr.repository.StoreRef#PROTOCOL_WORKSPACE} + * @param identifier a protocol-specific identifier + * @return Returns the new persistent entity + */ + public Store createStore(String protocol, String identifier); + + /** + * @param protocol the protocol that the store serves + * @param identifier the protocol-specific identifer + * @return Returns a store with the given values or null if one doesn't exist + */ + public Store getStore(String protocol, String identifier); + + /** + * @param store the store to which the node must belong + * @param id the node store-unique identifier + * @param nodeTypeQName the type of the node + * @return Returns a new node of the given type and attached to the store + * @throws InvalidTypeException if the node type is invalid or if the node type + * is not a valid real node + */ + public Node newNode(Store store, String id, QName nodeTypeQName) throws InvalidTypeException; + + /** + * @param protocol the store protocol + * @param identifier the store identifier for the given protocol + * @param id the store-specific node identifier + * @return Returns the node entity + */ + public Node getNode(String protocol, String identifier, String id); + + /** + * Deletes the node instance, taking care of any cascades that are required over + * and above those provided by the persistence mechanism. + *

    + * A caller must able to delete the node using this method and not have to follow + * up with any other ancillary deletes + * + * @param node the entity to delete + * @param cascade true if the assoc deletions must cascade to primary child nodes + */ + public void deleteNode(Node node, boolean cascade); + + /** + * @return Returns the persisted and filled association + * + * @see ChildAssoc + */ + public ChildAssoc newChildAssoc( + Node parentNode, + Node childNode, + boolean isPrimary, + QName assocTypeQName, + QName qname); + + /** + * @return Returns a matching association or null if one was not found + * + * @see ChildAssoc + */ + public ChildAssoc getChildAssoc( + Node parentNode, + Node childNode, + QName assocTypeQName, + QName qname); + + + /** + * @param assoc the child association to remove + * @param cascade true if the assoc deletions must cascade to primary child nodes + */ + public void deleteChildAssoc(ChildAssoc assoc, boolean cascade); + + /** + * Finds the association between the node's primary parent and the node itself + * + * @param node the child node + * @return Returns the primary ChildAssoc instance where the given node is the child. + * The return value could be null for a root node - but ONLY a root node + */ + public ChildAssoc getPrimaryParentAssoc(Node node); + + /** + * @return Returns the persisted and filled association + * @see NodeAssoc + */ + public NodeAssoc newNodeAssoc( + Node sourceNode, + Node targetNode, + QName assocTypeQName); + + /** + * @return Returns the node association or null if not found + */ + public NodeAssoc getNodeAssoc( + Node sourceNode, + Node targetNode, + QName assocTypeQName); + + /** + * @return Returns the target nodes for the association + */ + public Collection getNodeAssocTargets(Node sourceNode, QName assocTypeQName); + + /** + * @return Returns the source nodes for the association + */ + public Collection getNodeAssocSources(Node targetNode, QName assocTypeQName); + + /** + * @param assoc the node association to remove + */ + public void deleteNodeAssoc(NodeAssoc assoc); + + /** + * Gets the node's status. If the node never existed, then + * null is returned. + * + * @param protocol the store protocol + * @param identifier the store identifier for the given protocol + * @param id the store-specific node status identifier + * @return Returns the node status if the node exists or once existed, otherwise + * returns null. + */ + public NodeStatus getNodeStatus(String protocol, String identifier, String id); +} diff --git a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java new file mode 100644 index 0000000000..470871a58a --- /dev/null +++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.db.hibernate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.domain.NodeAssoc; +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.repo.domain.NodeStatus; +import org.alfresco.repo.domain.Store; +import org.alfresco.repo.domain.StoreKey; +import org.alfresco.repo.domain.hibernate.ChildAssocImpl; +import org.alfresco.repo.domain.hibernate.NodeAssocImpl; +import org.alfresco.repo.domain.hibernate.NodeImpl; +import org.alfresco.repo.domain.hibernate.NodeStatusImpl; +import org.alfresco.repo.domain.hibernate.StoreImpl; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.hibernate.ObjectDeletedException; +import org.hibernate.Query; +import org.hibernate.Session; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * Hibernate-specific implementation of the persistence-independent node DAO interface + * + * @author Derek Hulley + */ +public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements NodeDaoService +{ + public static final String QUERY_GET_ALL_STORES = "store.GetAllStores"; + public static final String QUERY_GET_CHILD_ASSOC = "node.GetChildAssoc"; + public static final String QUERY_GET_NODE_ASSOC = "node.GetNodeAssoc"; + public static final String QUERY_GET_NODE_ASSOC_TARGETS = "node.GetNodeAssocTargets"; + public static final String QUERY_GET_NODE_ASSOC_SOURCES = "node.GetNodeAssocSources"; + + /** a uuid identifying this unique instance */ + private String uuid; + + /** + * + */ + public HibernateNodeDaoServiceImpl() + { + this.uuid = GUID.generate(); + } + + /** + * Checks equality by type and uuid + */ + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (!(obj instanceof HibernateNodeDaoServiceImpl)) + { + return false; + } + HibernateNodeDaoServiceImpl that = (HibernateNodeDaoServiceImpl) obj; + return this.uuid.equals(that.uuid); + } + + /** + * @see #uuid + */ + public int hashCode() + { + return uuid.hashCode(); + } + + /** + * Does this Session contain any changes which must be + * synchronized with the store? + * + * @return true => changes are pending + */ + public boolean isDirty() + { + // create a callback for the task + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + return session.isDirty(); + } + }; + // execute the callback + return ((Boolean)getHibernateTemplate().execute(callback)).booleanValue(); + } + + /** + * @see #QUERY_GET_ALL_STORES + */ + @SuppressWarnings("unchecked") + public List getStores() + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_ALL_STORES); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + // done + return queryResults; + } + + /** + * Ensures that the store protocol/identifier combination is unique + */ + public Store createStore(String protocol, String identifier) + { + // ensure that the name isn't in use + Store store = getStore(protocol, identifier); + if (store != null) + { + throw new RuntimeException("A store already exists: \n" + + " protocol: " + protocol + "\n" + + " identifier: " + identifier + "\n" + + " store: " + store); + } + + store = new StoreImpl(); + // set key + store.setKey(new StoreKey(protocol, identifier)); + // persist so that it is present in the hibernate cache + getHibernateTemplate().save(store); + // create and assign a root node + Node rootNode = newNode( + store, + GUID.generate(), + ContentModel.TYPE_STOREROOT); + store.setRootNode(rootNode); + // done + return store; + } + + public Store getStore(String protocol, String identifier) + { + StoreKey storeKey = new StoreKey(protocol, identifier); + Store store = (Store) getHibernateTemplate().get(StoreImpl.class, storeKey); + // done + return store; + } + + public Node newNode(Store store, String id, QName nodeTypeQName) throws InvalidTypeException + { + NodeKey key = new NodeKey(store.getKey(), id); + + // create (or reuse) the mandatory node status + NodeStatus nodeStatus = (NodeStatus) getHibernateTemplate().get(NodeStatusImpl.class, key); + if (nodeStatus == null) + { + nodeStatus = new NodeStatusImpl(); + } + // set required status properties + nodeStatus.setKey(key); + nodeStatus.setDeleted(false); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + // persist the nodestatus + getHibernateTemplate().save(nodeStatus); + + // build a concrete node based on a bootstrap type + Node node = new NodeImpl(); + // set other required properties + node.setKey(key); + node.setTypeQName(nodeTypeQName); + node.setStore(store); + node.setStatus(nodeStatus); + // persist the node + getHibernateTemplate().save(node); + // done + return node; + } + + public Node getNode(String protocol, String identifier, String id) + { + try + { + NodeKey nodeKey = new NodeKey(protocol, identifier, id); + Object obj = getHibernateTemplate().get(NodeImpl.class, nodeKey); + // done + return (Node) obj; + } + catch (ObjectDeletedException e) + { + return null; + } + catch (DataAccessException e) + { + if (e.contains(ObjectDeletedException.class)) + { + // the object no loner exists + return null; + } + throw e; + } + } + + /** + * Manually ensures that all cascading of associations is taken care of + */ + public void deleteNode(Node node, boolean cascade) + { + // delete all parent assocs + Collection parentAssocs = node.getParentAssocs(); + parentAssocs = new ArrayList(parentAssocs); + for (ChildAssoc assoc : parentAssocs) + { + deleteChildAssoc(assoc, false); // we don't cascade upwards + } + // delete all child assocs + Collection childAssocs = node.getChildAssocs(); + childAssocs = new ArrayList(childAssocs); + for (ChildAssoc assoc : childAssocs) + { + deleteChildAssoc(assoc, cascade); // potentially cascade downwards + } + // delete all target assocs + Collection targetAssocs = node.getTargetNodeAssocs(); + targetAssocs = new ArrayList(targetAssocs); + for (NodeAssoc assoc : targetAssocs) + { + deleteNodeAssoc(assoc); + } + // delete all source assocs + Collection sourceAssocs = node.getSourceNodeAssocs(); + sourceAssocs = new ArrayList(sourceAssocs); + for (NodeAssoc assoc : sourceAssocs) + { + deleteNodeAssoc(assoc); + } + // update the node status + NodeStatus nodeStatus = node.getStatus(); + nodeStatus.setDeleted(true); + nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); + // finally delete the node + getHibernateTemplate().delete(node); + // done + } + + /** + * Fetch the node status, if it exists + */ + public NodeStatus getNodeStatus(String protocol, String identifier, String id) + { + try + { + NodeKey nodeKey = new NodeKey(protocol, identifier, id); + Object obj = getHibernateTemplate().get(NodeStatusImpl.class, nodeKey); + // done + return (NodeStatus) obj; + } + catch (DataAccessException e) + { + if (e.contains(ObjectDeletedException.class)) + { + // the object no loner exists + return null; + } + throw e; + } + } + + public ChildAssoc newChildAssoc( + Node parentNode, + Node childNode, + boolean isPrimary, + QName assocTypeQName, + QName qname) + { + ChildAssoc assoc = new ChildAssocImpl(); + assoc.setTypeQName(assocTypeQName); + assoc.setIsPrimary(isPrimary); + assoc.setQname(qname); + assoc.buildAssociation(parentNode, childNode); + // persist + getHibernateTemplate().save(assoc); + // done + return assoc; + } + + public ChildAssoc getChildAssoc( + Node parentNode, + Node childNode, + QName assocTypeQName, + QName qname) + { + ChildAssociationRef childAssocRef = new ChildAssociationRef( + assocTypeQName, + parentNode.getNodeRef(), + qname, + childNode.getNodeRef()); + // get all the parent's child associations + Collection assocs = parentNode.getChildAssocs(); + // hunt down the desired assoc + for (ChildAssoc assoc : assocs) + { + // is it a match? + if (!assoc.getChildAssocRef().equals(childAssocRef)) // not a match + { + continue; + } + else + { + return assoc; + } + } + // not found + return null; + } + + /** + * Manually enforces cascade deletions down primary associations + */ + public void deleteChildAssoc(ChildAssoc assoc, boolean cascade) + { + Node childNode = assoc.getChild(); + + // maintain inverse association sets + assoc.removeAssociation(); + // remove instance + getHibernateTemplate().delete(assoc); + + if (cascade && assoc.getIsPrimary()) // the assoc is primary + { + // delete the child node + deleteNode(childNode, cascade); + /* + * The child node deletion will cascade delete all assocs to + * and from it, but we have safely removed this one, so no + * duplicate call will be received to do this + */ + } + } + + public ChildAssoc getPrimaryParentAssoc(Node node) + { + // get the assocs pointing to the node + Collection parentAssocs = node.getParentAssocs(); + ChildAssoc primaryAssoc = null; + for (ChildAssoc assoc : parentAssocs) + { + // ignore non-primary assocs + if (!assoc.getIsPrimary()) + { + continue; + } + else if (primaryAssoc != null) + { + // we have more than one somehow + throw new DataIntegrityViolationException( + "Multiple primary associations: \n" + + " child: " + node + "\n" + + " first primary assoc: " + primaryAssoc + "\n" + + " second primary assoc: " + assoc); + } + primaryAssoc = assoc; + // we keep looping to hunt out data integrity issues + } + // did we find a primary assoc? + if (primaryAssoc == null) + { + // the only condition where this is allowed is if the given node is a root node + Store store = node.getStore(); + Node rootNode = store.getRootNode(); + if (rootNode == null) + { + // a store without a root node - the entire store is hosed + throw new DataIntegrityViolationException("Store has no root node: \n" + + " store: " + store); + } + if (!rootNode.equals(node)) + { + // it wasn't the root node + throw new DataIntegrityViolationException("Non-root node has no primary parent: \n" + + " child: " + node); + } + } + // done + return primaryAssoc; + } + + public NodeAssoc newNodeAssoc(Node sourceNode, Node targetNode, QName assocTypeQName) + { + NodeAssoc assoc = new NodeAssocImpl(); + assoc.setTypeQName(assocTypeQName); + assoc.buildAssociation(sourceNode, targetNode); + // persist + getHibernateTemplate().save(assoc); + // done + return assoc; + } + + public NodeAssoc getNodeAssoc( + final Node sourceNode, + final Node targetNode, + final QName assocTypeQName) + { + final NodeKey sourceKey = sourceNode.getKey(); + final NodeKey targetKey = targetNode.getKey(); + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_NODE_ASSOC); + query.setString("sourceKeyProtocol", sourceKey.getProtocol()) + .setString("sourceKeyIdentifier", sourceKey.getIdentifier()) + .setString("sourceKeyGuid", sourceKey.getGuid()) + .setString("assocTypeQName", assocTypeQName.toString()) + .setString("targetKeyProtocol", targetKey.getProtocol()) + .setString("targetKeyIdentifier", targetKey.getIdentifier()) + .setString("targetKeyGuid", targetKey.getGuid()); + query.setMaxResults(1); + return query.uniqueResult(); + } + }; + Object queryResult = getHibernateTemplate().execute(callback); + if (queryResult == null) + { + return null; + } + NodeAssoc assoc = (NodeAssoc) queryResult; + // done + return assoc; + } + + @SuppressWarnings("unchecked") + public Collection getNodeAssocTargets(final Node sourceNode, final QName assocTypeQName) + { + final NodeKey sourceKey = sourceNode.getKey(); + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_NODE_ASSOC_TARGETS); + query.setString("sourceKeyProtocol", sourceKey.getProtocol()) + .setString("sourceKeyIdentifier", sourceKey.getIdentifier()) + .setString("sourceKeyGuid", sourceKey.getGuid()) + .setString("assocTypeQName", assocTypeQName.toString()); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + // done + return queryResults; + } + + @SuppressWarnings("unchecked") + public Collection getNodeAssocSources(final Node targetNode, final QName assocTypeQName) + { + final NodeKey targetKey = targetNode.getKey(); + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_NODE_ASSOC_SOURCES); + query.setString("targetKeyProtocol", targetKey.getProtocol()) + .setString("targetKeyIdentifier", targetKey.getIdentifier()) + .setString("targetKeyGuid", targetKey.getGuid()) + .setString("assocTypeQName", assocTypeQName.toString()); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + // done + return queryResults; + } + + public void deleteNodeAssoc(NodeAssoc assoc) + { + // maintain inverse association sets + assoc.removeAssociation(); + // remove instance + getHibernateTemplate().delete(assoc); + } +} diff --git a/source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponent.java b/source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponent.java new file mode 100644 index 0000000000..10e20d4796 --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponent.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Ensures that the FTS indexing picks up on any outstanding documents that + * require indexing. + *

    + * FTS indexing is a background process. It is therefore possible that + * certain documents don't get indexed when the server shuts down. + * + * @author Derek Hulley + */ +public class FtsIndexRecoveryComponent implements IndexRecovery +{ + private static Log logger = LogFactory.getLog(FtsIndexRecoveryComponent.class); + + /** provides transactions to atomically index each missed transaction */ + private TransactionService transactionService; + /** the FTS indexer that we will prompt to pick up on any un-indexed text */ + private FullTextSearchIndexer ftsIndexer; + /** the component providing searches of the indexed nodes */ + private SearchService searcher; + /** the component giving direct access to node instances */ + private NodeService nodeService; + /** the workspaces to reindex */ + private List storeRefs; + + public FtsIndexRecoveryComponent() + { + this.storeRefs = new ArrayList(2); + } + + /** + * @param transactionService provide transactions to index each missed transaction + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * @param ftsIndexer the FTS background indexer + */ + public void setFtsIndexer(FullTextSearchIndexer ftsIndexer) + { + this.ftsIndexer = ftsIndexer; + } + + /** + * @param nodeService provides information about nodes for indexing + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the workspaces that need reindexing + * + * @param storeRefStrings a list of strings representing store references + */ + public void setStores(List storeRefStrings) + { + storeRefs.clear(); + for (String storeRefStr : storeRefStrings) + { + StoreRef storeRef = new StoreRef(storeRefStr); + storeRefs.add(storeRef); + } + } + + /** + * Ensures that the FTS indexing is activated for any outstanding full text searches. + */ + public void reindex() + { + TransactionWork reindexWork = new TransactionWork() + { + public Object doWork() + { + // reindex each store + for (StoreRef storeRef : storeRefs) + { + // check if the store exists + if (!nodeService.exists(storeRef)) + { + // store does not exist + if (logger.isDebugEnabled()) + { + logger.debug("Skipping reindex of non-existent store: " + storeRef); + } + continue; + } + + // prompt FTS to reindex the store + ftsIndexer.requiresIndex(storeRef); + } + // done + return null; + } + }; + TransactionUtil.executeInUserTransaction(transactionService, reindexWork); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Prompted FTS index on stores: " + storeRefs); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponentTest.java b/source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponentTest.java new file mode 100644 index 0000000000..70d77bfa49 --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/FtsIndexRecoveryComponentTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +import junit.framework.TestCase; + +import org.alfresco.repo.search.Indexer; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * Checks that the FTS index recovery component is working + * + * @author Derek Hulley + */ +public class FtsIndexRecoveryComponentTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private IndexRecovery indexRecoverer; + private NodeService nodeService; + private TransactionService txnService; + private Indexer indexer; + + public void setUp() throws Exception + { + indexRecoverer = (IndexRecovery) ctx.getBean("indexRecoveryComponent"); + txnService = (TransactionService) ctx.getBean("transactionComponent"); + nodeService = (NodeService) ctx.getBean("nodeService"); + indexer = (Indexer) ctx.getBean("indexerComponent"); + } + + public void testReindexing() throws Exception + { + // performs a reindex + TransactionWork reindexWork = new TransactionWork() + { + public Object doWork() + { + indexRecoverer.reindex(); + return null; + } + }; + + // reindex + TransactionUtil.executeInNonPropagatingUserTransaction(txnService, reindexWork); + } +} diff --git a/source/java/org/alfresco/repo/node/index/IndexRecovery.java b/source/java/org/alfresco/repo/node/index/IndexRecovery.java new file mode 100644 index 0000000000..887b427064 --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/IndexRecovery.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +/** + * Interface for components able to recover indexes. + * + * @author Derek Hulley + */ +public interface IndexRecovery +{ + /** + * Forces a reindex + */ + public void reindex(); +} diff --git a/source/java/org/alfresco/repo/node/index/IndexRecoveryJob.java b/source/java/org/alfresco/repo/node/index/IndexRecoveryJob.java new file mode 100644 index 0000000000..1c61ca8446 --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/IndexRecoveryJob.java @@ -0,0 +1,33 @@ +package org.alfresco.repo.node.index; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Forces a index recovery using the {@link IndexRecovery recovery component} passed + * in via the job detail. + * + * @author Derek Hulley + */ +public class IndexRecoveryJob implements Job +{ + /** KEY_INDEX_RECOVERY_COMPONENT = 'indexRecoveryComponent' */ + public static final String KEY_INDEX_RECOVERY_COMPONENT = "indexRecoveryComponent"; + + /** + * Forces a full index recovery using the {@link IndexRecovery recovery component} passed + * in via the job detail. + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + IndexRecovery indexRecoveryComponent = (IndexRecovery) context.getJobDetail() + .getJobDataMap().get(KEY_INDEX_RECOVERY_COMPONENT); + if (indexRecoveryComponent == null) + { + throw new JobExecutionException("Missing job data: " + KEY_INDEX_RECOVERY_COMPONENT); + } + // reindex + indexRecoveryComponent.reindex(); + } +} diff --git a/source/java/org/alfresco/repo/node/index/NodeIndexer.java b/source/java/org/alfresco/repo/node/index/NodeIndexer.java new file mode 100644 index 0000000000..4005ab375e --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/NodeIndexer.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.search.Indexer; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Handles the node policy callbacks to ensure that the node hierarchy is properly + * indexed. + * + * @author Derek Hulley + */ +public class NodeIndexer + implements NodeServicePolicies.BeforeCreateStorePolicy, + NodeServicePolicies.OnCreateNodePolicy, + NodeServicePolicies.OnUpdateNodePolicy, + NodeServicePolicies.OnDeleteNodePolicy, + NodeServicePolicies.OnCreateChildAssociationPolicy, + NodeServicePolicies.OnDeleteChildAssociationPolicy +{ + /** the component to register the behaviour with */ + private PolicyComponent policyComponent; + /** the component to index the node hierarchy */ + private Indexer indexer; + + /** + * @param policyComponent used for registrations + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param indexer the indexer that will be index + */ + public void setIndexer(Indexer indexer) + { + this.indexer = indexer; + } + + /** + * Registers the policy behaviour methods + */ + private void init() + { + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "beforeCreateStore"), + ContentModel.TYPE_STOREROOT, + new JavaBehaviour(this, "beforeCreateStore")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), + this, + new JavaBehaviour(this, "onCreateNode")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateNode"), + this, + new JavaBehaviour(this, "onUpdateNode")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteNode"), + this, + new JavaBehaviour(this, "onDeleteNode")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + this, + new JavaBehaviour(this, "onCreateChildAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteChildAssociation"), + this, + new JavaBehaviour(this, "onDeleteChildAssociation")); + } + + public void beforeCreateStore(QName nodeTypeQName, StoreRef storeRef) + { + // indexer can perform some cleanup here, if required + } + + public void onCreateNode(ChildAssociationRef childAssocRef) + { + indexer.createNode(childAssocRef); + } + + public void onUpdateNode(NodeRef nodeRef) + { + indexer.updateNode(nodeRef); + } + + public void onDeleteNode(ChildAssociationRef childAssocRef) + { + indexer.deleteNode(childAssocRef); + } + + public void onCreateChildAssociation(ChildAssociationRef childAssocRef) + { + indexer.createChildRelationship(childAssocRef); + } + + public void onDeleteChildAssociation(ChildAssociationRef childAssocRef) + { + indexer.deleteChildRelationship(childAssocRef); + } +} diff --git a/source/java/org/alfresco/repo/node/index/NodeIndexerTest.java b/source/java/org/alfresco/repo/node/index/NodeIndexerTest.java new file mode 100644 index 0000000000..fed09c179a --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/NodeIndexerTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.perf.PerformanceMonitor; + +/** + * Checks that the indexing of the node hierarchy is working + * + * @see org.alfresco.repo.node.index.NodeIndexer + * + * @author Derek Hulley + */ +public class NodeIndexerTest extends BaseNodeServiceTest +{ + private SearchService searchService; + private static StoreRef localStoreRef; + private static NodeRef localRootNode; + + @Override + protected NodeService getNodeService() + { + return (NodeService) applicationContext.getBean("nodeService"); + } + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + searchService = (SearchService) applicationContext.getBean("searchService"); + + if (localStoreRef == null) + { + localStoreRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_Persisted" + System.currentTimeMillis()); + localRootNode = nodeService.getRootNode(localStoreRef); + } + } + + public void testCommitQueryData() throws Exception + { + rootNodeRef = localRootNode; + buildNodeGraph(); + setComplete(); + } + + public void testQuery() throws Exception + { + rootNodeRef = localRootNode; + ResultSet results = searchService.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"" + BaseNodeServiceTest.TEST_PREFIX + ":root_p_n1\"", null, null); + assertEquals(1, results.length()); + results.close(); + } + + public void testLikeAndContains() throws Exception + { + rootNodeRef = localRootNode; + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace(NamespaceService.SYSTEM_MODEL_PREFIX, NamespaceService.SYSTEM_MODEL_1_0_URI); + namespacePrefixResolver.registerNamespace(NamespaceService.CONTENT_MODEL_PREFIX, NamespaceService.CONTENT_MODEL_1_0_URI); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + + PerformanceMonitor selectNodesPerf = new PerformanceMonitor(getClass().getSimpleName(), "selectNodes"); + PerformanceMonitor selectPropertiesPerf = new PerformanceMonitor(getClass().getSimpleName(), "selectProperties"); + + List answer; + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[like(@test:animal, 'm_nkey')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[like(@test:animal, 'm%key')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[like(@test:animal, 'monk__')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[like(@test:animal, 'monk%')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[like(@test:animal, 'monk\\%')]", null, namespacePrefixResolver, false); + assertEquals(0, answer.size()); + selectNodesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[contains('monkey')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectPropertiesPerf.start(); + List result = searchService.selectProperties(rootNodeRef, "//@*[contains('monkey')]", null, namespacePrefixResolver, false); + assertEquals(2, result.size()); + selectPropertiesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[contains('mon?ey')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectPropertiesPerf.start(); + result = searchService.selectProperties(rootNodeRef, "//@*[contains('mon?ey')]", null, namespacePrefixResolver, false); + assertEquals(2, result.size()); + selectPropertiesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[contains('m*y')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectPropertiesPerf.start(); + result = searchService.selectProperties(rootNodeRef, "//@*[contains('mon*')]", null, namespacePrefixResolver, false); + assertEquals(2, result.size()); + selectPropertiesPerf.stop(); + + selectNodesPerf.start(); + answer = searchService.selectNodes(rootNodeRef, "//*[contains('*nkey')]", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + selectNodesPerf.stop(); + + selectPropertiesPerf.start(); + result = searchService.selectProperties(rootNodeRef, "//@*[contains('?onkey')]", null, namespacePrefixResolver, false); + assertEquals(2, result.size()); + selectPropertiesPerf.stop(); + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/AbstractIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AbstractIntegrityEvent.java new file mode 100644 index 0000000000..5660577f98 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/AbstractIntegrityEvent.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; + +/** + * Base class for integrity events. It provides basic support for checking + * model integrity. + * + * @author Derek Hulley + */ +public abstract class AbstractIntegrityEvent implements IntegrityEvent +{ + protected final NodeService nodeService; + protected final DictionaryService dictionaryService; + + /** the potential problem traces */ + private List traces; + /** support for derived classes */ + private final NodeRef nodeRef; + /** support for derived classes */ + private final QName typeQName; + /** support for derived classes */ + private final QName qname; + + /** cached hashcode as the members are all final */ + private int hashCode = 0; + + /** + * Constructor with helper values for storage + */ + protected AbstractIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef nodeRef, + QName typeQName, + QName qname) + { + this.nodeService = nodeService; + this.dictionaryService = dictionaryService; + this.traces = new ArrayList(0); + + this.nodeRef = nodeRef; + this.typeQName = typeQName; + this.qname = qname; + } + + @Override + public int hashCode() + { + if (hashCode == 0) + { + hashCode = + 0 + + 1 * (nodeRef == null ? 0 : nodeRef.hashCode()) + - 17* (typeQName == null ? 0 : typeQName.hashCode()) + + 17* (qname == null ? 0 : qname.hashCode()); + } + return hashCode; + } + + /** + * Compares based on the class of this instance and the incoming instance, before + * comparing based on all the internal data. If derived classes store additional + * data for their functionality, then they should override this. + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + return false; + else if (this == obj) + return true; + else if (this.getClass() != obj.getClass()) + return false; + // we can safely cast + AbstractIntegrityEvent that = (AbstractIntegrityEvent) obj; + return + EqualsHelper.nullSafeEquals(this.nodeRef, that.nodeRef) && + EqualsHelper.nullSafeEquals(this.typeQName, that.typeQName) && + EqualsHelper.nullSafeEquals(this.qname, that.qname); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(56); + sb.append("IntegrityEvent") + .append("[ name=").append(getClass().getName()); + if (nodeRef != null) + sb.append(", nodeRef=").append(nodeRef); + if (typeQName != null) + sb.append(", typeQName=").append(typeQName); + if (qname != null) + sb.append(", qname=").append(qname); + sb.append("]"); + // done + return sb.toString(); + } + + /** + * Gets the node type if the node exists + * + * @param nodeRef + * @return Returns the node's type or null if the node no longer exists + */ + protected QName getNodeType(NodeRef nodeRef) + { + try + { + return nodeService.getType(nodeRef); + } + catch (InvalidNodeRefException e) + { + // node has disappeared + return null; + } + } + + /** + * @return Returns the traces (if present) that caused the creation of this event + */ + public List getTraces() + { + return traces; + } + + public void addTrace(StackTraceElement[] trace) + { + traces.add(trace); + } + + protected NodeRef getNodeRef() + { + return nodeRef; + } + + protected QName getTypeQName() + { + return typeQName; + } + + protected QName getQName() + { + return qname; + } + + /** + * Gets the association definition from the dictionary. If the source node type is + * provided then the association particular to the subtype is attempted. + * + * @param eventResults results to add a violation message to + * @param assocTypeQName the type of the association + * @return Returns the association definition, or null if not found + */ + protected AssociationDefinition getAssocDef(List eventResults, QName assocTypeQName) + { + return dictionaryService.getAssociation(assocTypeQName); + } + + protected String getMultiplicityString(boolean mandatory, boolean allowMany) + { + StringBuilder sb = new StringBuilder(4); + sb.append(mandatory ? "1" : "0"); + sb.append(".."); + sb.append(allowMany ? "*" : "1"); + return sb.toString(); + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/AssocSourceMultiplicityIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AssocSourceMultiplicityIntegrityEvent.java new file mode 100644 index 0000000000..9f1d91bdc4 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/AssocSourceMultiplicityIntegrityEvent.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Event raised to check the source multiplicity for an association type + * from the given node. + *

    + * Checks are ignored is the target node doesn't exist. + * + * @author Derek Hulley + */ +public class AssocSourceMultiplicityIntegrityEvent extends AbstractIntegrityEvent +{ + private static Log logger = LogFactory.getLog(AssocSourceMultiplicityIntegrityEvent.class); + + /** true if the assoc type may not be valid, e.g. during association deletions */ + private boolean isDelete; + + public AssocSourceMultiplicityIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef targetNodeRef, + QName assocTypeQName, + boolean isDelete) + { + super(nodeService, dictionaryService, targetNodeRef, assocTypeQName, null); + this.isDelete = isDelete; + } + + @Override + public boolean equals(Object obj) + { + if (!super.equals(obj)) + { + return false; + } + // so far, so good + AssocSourceMultiplicityIntegrityEvent that = (AssocSourceMultiplicityIntegrityEvent) obj; + return this.isDelete == that.isDelete; + } + + public void checkIntegrity(List eventResults) + { + QName assocTypeQName = getTypeQName(); + NodeRef targetNodeRef = getNodeRef(); + // event is irrelevant if the node is gone + QName targetNodeTypeQName = getNodeType(targetNodeRef); + if (targetNodeTypeQName == null) + { + // target or source is missing + if (logger.isDebugEnabled()) + { + logger.debug("Ignoring integrity check - node gone: \n" + + " event: " + this); + } + return; + } + + // get the association def + AssociationDefinition assocDef = getAssocDef(eventResults, assocTypeQName); + // the association definition must exist + if (assocDef == null) + { + if (!isDelete) // strict about the type + { + IntegrityRecord result = new IntegrityRecord( + "Association type does not exist: \n" + + " Target Node Type: " + targetNodeTypeQName + "\n" + + " Association Type: " + assocTypeQName); + eventResults.add(result); + return; + } + else // not strict about the type + { + return; + } + } + + // perform required checks + checkSourceMultiplicity(eventResults, assocDef, assocTypeQName, targetNodeRef); + } + + /** + * Checks that the source multiplicity has not been violated for the + * target of the association. + */ + protected void checkSourceMultiplicity( + List eventResults, + AssociationDefinition assocDef, + QName assocTypeQName, + NodeRef targetNodeRef) + { + // get the source multiplicity + boolean mandatory = assocDef.isSourceMandatory(); + boolean allowMany = assocDef.isSourceMany(); + // do we need to check + if (!mandatory && allowMany) + { + // it is not mandatory and it allows many on both sides of the assoc + return; + } + int actualSize = 0; + if (assocDef.isChild()) + { + // check the parent assocs present + List parentAssocRefs = nodeService.getParentAssocs( + targetNodeRef, + assocTypeQName, + RegexQNamePattern.MATCH_ALL); + actualSize = parentAssocRefs.size(); + } + else + { + // check the source assocs present + List sourceAssocRefs = nodeService.getSourceAssocs(targetNodeRef, assocTypeQName); + actualSize = sourceAssocRefs.size(); + } + if ((mandatory && actualSize == 0) || (!allowMany && actualSize > 1)) + { + String parentOrSourceStr = (assocDef.isChild() ? "child" : "target"); + IntegrityRecord result = new IntegrityRecord( + "The association " + parentOrSourceStr + " multiplicity has been violated: \n" + + " Association: " + assocDef + "\n" + + " Required " + parentOrSourceStr + " Multiplicity: " + getMultiplicityString(mandatory, allowMany) + "\n" + + " Actual " + parentOrSourceStr + " Multiplicity: " + actualSize); + eventResults.add(result); + } + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/AssocSourceTypeIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AssocSourceTypeIntegrityEvent.java new file mode 100644 index 0000000000..22f2b42653 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/AssocSourceTypeIntegrityEvent.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Event to check the source type of an association + *

    + * Checks are ignored if the source node has been deleted. + * + * @author Derek Hulley + */ +public class AssocSourceTypeIntegrityEvent extends AbstractIntegrityEvent +{ + private static Log logger = LogFactory.getLog(AssocSourceTypeIntegrityEvent.class); + + public AssocSourceTypeIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef sourceNodeRef, + QName assocTypeQName) + { + super(nodeService, dictionaryService, sourceNodeRef, assocTypeQName, null); + } + + public void checkIntegrity(List eventResults) + { + QName assocTypeQName = getTypeQName(); + NodeRef sourceNodeRef = getNodeRef(); + // if the node is gone then the check is irrelevant + QName sourceNodeTypeQName = getNodeType(sourceNodeRef); + if (sourceNodeTypeQName == null) + { + // target or source is missing + if (logger.isDebugEnabled()) + { + logger.debug("Ignoring integrity check - node gone: \n" + + " event: " + this); + } + return; + } + + // get the association def + AssociationDefinition assocDef = getAssocDef(eventResults, assocTypeQName); + // the association definition must exist + if (assocDef == null) + { + IntegrityRecord result = new IntegrityRecord( + "Association type does not exist: \n" + + " Source Node Type: " + sourceNodeTypeQName + "\n" + + " Association Type: " + assocTypeQName); + eventResults.add(result); + return; + } + + // perform required checks + checkSourceType(eventResults, assocDef, sourceNodeRef, sourceNodeTypeQName); + } + + /** + * Checks that the source node type is valid for the association. + */ + protected void checkSourceType( + List eventResults, + AssociationDefinition assocDef, + NodeRef sourceNodeRef, + QName sourceNodeTypeQName) + { + // check the association source type + ClassDefinition sourceDef = assocDef.getSourceClass(); + if (sourceDef instanceof TypeDefinition) + { + // the node type must be a match + if (!dictionaryService.isSubClass(sourceNodeTypeQName, sourceDef.getName())) + { + IntegrityRecord result = new IntegrityRecord( + "The association source type is incorrect: \n" + + " Association: " + assocDef + "\n" + + " Required Source Type: " + sourceDef.getName() + "\n" + + " Actual Source Type: " + sourceNodeTypeQName); + eventResults.add(result); + } + } + else if (sourceDef instanceof AspectDefinition) + { + // the source must have a relevant aspect + Set sourceAspects = nodeService.getAspects(sourceNodeRef); + boolean found = false; + for (QName sourceAspectTypeQName : sourceAspects) + { + if (dictionaryService.isSubClass(sourceAspectTypeQName, sourceDef.getName())) + { + found = true; + break; + } + } + if (!found) + { + IntegrityRecord result = new IntegrityRecord( + "The association source is missing the aspect required for this association: \n" + + " Association: " + assocDef + "\n" + + " Required Source Aspect: " + sourceDef.getName() + "\n" + + " Actual Source Aspects: " + sourceAspects); + eventResults.add(result); + } + } + else + { + IntegrityRecord result = new IntegrityRecord( + "Unknown ClassDefinition subclass on the source definition: \n" + + " Association: " + assocDef + "\n" + + " Source Definition: " + sourceDef.getName()); + eventResults.add(result); + } + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/AssocTargetMultiplicityIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AssocTargetMultiplicityIntegrityEvent.java new file mode 100644 index 0000000000..2447432308 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/AssocTargetMultiplicityIntegrityEvent.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Event raised to check the target multiplicity for an association type + * from the given node. + *

    + * Checks are ignored is the target node doesn't exist. + * + * @author Derek Hulley + */ +public class AssocTargetMultiplicityIntegrityEvent extends AbstractIntegrityEvent +{ + private static Log logger = LogFactory.getLog(AssocTargetMultiplicityIntegrityEvent.class); + + /** true if the assoc type may not be valid, e.g. during association deletions */ + private boolean isDelete; + + public AssocTargetMultiplicityIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef sourceNodeRef, + QName assocTypeQName, + boolean isDelete) + { + super(nodeService, dictionaryService, sourceNodeRef, assocTypeQName, null); + this.isDelete = isDelete; + } + + @Override + public boolean equals(Object obj) + { + if (!super.equals(obj)) + { + return false; + } + // so far, so good + AssocTargetMultiplicityIntegrityEvent that = (AssocTargetMultiplicityIntegrityEvent) obj; + return this.isDelete == that.isDelete; + } + + public void checkIntegrity(List eventResults) + { + QName assocTypeQName = getTypeQName(); + NodeRef sourceNodeRef = getNodeRef(); + // event is irrelevant if the node is gone + QName sourceNodeTypeQName = getNodeType(sourceNodeRef); + if (sourceNodeTypeQName == null) + { + // target or target is missing + if (logger.isDebugEnabled()) + { + logger.debug("Ignoring integrity check - node gone: \n" + + " event: " + this); + } + return; + } + + // get the association def + AssociationDefinition assocDef = getAssocDef(eventResults, assocTypeQName); + // the association definition must exist + if (assocDef == null) + { + if (!isDelete) // strict about the type + { + IntegrityRecord result = new IntegrityRecord( + "Association type does not exist: \n" + + " Source Node Type: " + sourceNodeTypeQName + "\n" + + " Association Type: " + assocTypeQName); + eventResults.add(result); + return; + } + else // not strict about the type + { + return; + } + } + + // perform required checks + checkTargetMultiplicity(eventResults, assocDef, assocTypeQName, sourceNodeRef); + } + + /** + * Checks that the target multiplicity has not been violated for the + * source of the association. + */ + protected void checkTargetMultiplicity( + List eventResults, + AssociationDefinition assocDef, + QName assocTypeQName, + NodeRef sourceNodeRef) + { + // get the source multiplicity + boolean mandatory = assocDef.isTargetMandatory(); + boolean allowMany = assocDef.isTargetMany(); + // do we need to check + if (!mandatory && allowMany) + { + // it is not mandatory and it allows many on both sides of the assoc + return; + } + int actualSize = 0; + if (assocDef.isChild()) + { + // check the child assocs present + List childAssocRefs = nodeService.getChildAssocs( + sourceNodeRef, + assocTypeQName, + RegexQNamePattern.MATCH_ALL); + actualSize = childAssocRefs.size(); + } + else + { + // check the target assocs present + List targetAssocRefs = nodeService.getTargetAssocs(sourceNodeRef, assocTypeQName); + actualSize = targetAssocRefs.size(); + } + if ((mandatory && actualSize == 0) || (!allowMany && actualSize > 1)) + { + String childOrTargetStr = (assocDef.isChild() ? "child" : "target"); + IntegrityRecord result = new IntegrityRecord( + "The association " + childOrTargetStr + " multiplicity has been violated: \n" + + " Association: " + assocDef + "\n" + + " Required " + childOrTargetStr + " Multiplicity: " + getMultiplicityString(mandatory, allowMany) + "\n" + + " Actual " + childOrTargetStr + " Multiplicity: " + actualSize); + eventResults.add(result); + } + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java new file mode 100644 index 0000000000..8661e22e38 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Event to check the association target role name + * + * @author Derek Hulley + */ +public class AssocTargetRoleIntegrityEvent extends AbstractIntegrityEvent +{ + private static Log logger = LogFactory.getLog(AssocTargetRoleIntegrityEvent.class); + + public AssocTargetRoleIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef sourceNodeRef, + QName assocTypeQName, + QName assocName) + { + super(nodeService, dictionaryService, sourceNodeRef, assocTypeQName, assocName); + } + + public void checkIntegrity(List eventResults) + { + NodeRef sourceNodeRef = getNodeRef(); + QName assocTypeQName = getTypeQName(); + QName assocQName = getQName(); + + // get the association def + AssociationDefinition assocDef = getAssocDef(eventResults, assocTypeQName); + // the association definition must exist + if (assocDef == null) + { + IntegrityRecord result = new IntegrityRecord( + "Association type does not exist: \n" + + " Association Type: " + assocTypeQName); + eventResults.add(result); + return; + } + + // check that we are dealing with child associations + if (assocQName == null) + { + throw new IllegalArgumentException("The association qualified name must be supplied"); + } + if (!assocDef.isChild()) + { + throw new UnsupportedOperationException("This operation is only relevant to child associations"); + } + ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef; + + // perform required checks + checkAssocQNameRegex(eventResults, childAssocDef, assocQName); + checkAssocQNameDuplicate(eventResults, childAssocDef, sourceNodeRef, assocQName); + } + + /** + * Checks that the association name matches the constraints imposed by the model. + */ + protected void checkAssocQNameRegex( + List eventResults, + ChildAssociationDefinition assocDef, + QName assocQName) + { + // check the association name + QName assocRoleQName = assocDef.getTargetRoleName(); + if (assocRoleQName != null) + { + // the assoc defines a role name - check it + RegexQNamePattern rolePattern = new RegexQNamePattern(assocRoleQName.toString()); + if (!rolePattern.isMatch(assocQName)) + { + IntegrityRecord result = new IntegrityRecord( + "The association name does not match the allowed role names: \n" + + " Association: " + assocDef + "\n" + + " Allowed roles: " + rolePattern + "\n" + + " Name assigned: " + assocRoleQName); + eventResults.add(result); + } + } + } + + /** + * Checks that the association name matches the constraints imposed by the model. + */ + protected void checkAssocQNameDuplicate( + List eventResults, + ChildAssociationDefinition assocDef, + NodeRef sourceNodeRef, + QName assocQName) + { + if (assocDef.getDuplicateChildNamesAllowed()) + { + // nothing to do + return; + } + QName assocTypeQName = assocDef.getName(); + // see if there is another association with the same name + try + { + List childAssocs = nodeService.getChildAssocs(sourceNodeRef, assocTypeQName, assocQName); + // duplicates not allowed + if (childAssocs.size() > 1) + { + IntegrityRecord result = new IntegrityRecord( + "Duplicate child associations are not allowed: \n" + + " Association: " + assocDef + "\n" + + " Name: " + assocQName); + eventResults.add(result); + } + } + catch (InvalidNodeRefException e) + { + // node has gone + } + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/AssocTargetTypeIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AssocTargetTypeIntegrityEvent.java new file mode 100644 index 0000000000..44981322d9 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/AssocTargetTypeIntegrityEvent.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Event to check the target type of an association + *

    + * Checks are ignored if the target node has been deleted. + * + * @author Derek Hulley + */ +public class AssocTargetTypeIntegrityEvent extends AbstractIntegrityEvent +{ + private static Log logger = LogFactory.getLog(AssocTargetTypeIntegrityEvent.class); + + public AssocTargetTypeIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef targetNodeRef, + QName assocTypeQName) + { + super(nodeService, dictionaryService, targetNodeRef, assocTypeQName, null); + } + + public void checkIntegrity(List eventResults) + { + QName assocTypeQName = getTypeQName(); + NodeRef targetNodeRef = getNodeRef(); + // if the node is gone then the check is irrelevant + QName targetNodeTypeQName = getNodeType(targetNodeRef); + if (targetNodeTypeQName == null) + { + // target or source is missing + if (logger.isDebugEnabled()) + { + logger.debug("Ignoring integrity check - node gone: \n" + + " event: " + this); + } + return; + } + + // get the association def + AssociationDefinition assocDef = getAssocDef(eventResults, assocTypeQName); + // the association definition must exist + if (assocDef == null) + { + IntegrityRecord result = new IntegrityRecord( + "Association type does not exist: \n" + + " Target Node Type: " + targetNodeTypeQName + "\n" + + " Association Type: " + assocTypeQName); + eventResults.add(result); + return; + } + + // perform required checks + checkTargetType(eventResults, assocDef, targetNodeRef, targetNodeTypeQName); + } + + /** + * Checks that the target node type is valid for the association. + */ + protected void checkTargetType( + List eventResults, + AssociationDefinition assocDef, + NodeRef targetNodeRef, + QName targetNodeTypeQName) + { + // check the association target type + ClassDefinition targetDef = assocDef.getTargetClass(); + if (targetDef instanceof TypeDefinition) + { + // the node type must be a match + if (!dictionaryService.isSubClass(targetNodeTypeQName, targetDef.getName())) + { + IntegrityRecord result = new IntegrityRecord( + "The association target type is incorrect: \n" + + " Association: " + assocDef + "\n" + + " Required Target Type: " + targetDef.getName() + "\n" + + " Actual Target Type: " + targetNodeTypeQName); + eventResults.add(result); + } + } + else if (targetDef instanceof AspectDefinition) + { + // the target must have a relevant aspect + Set targetAspects = nodeService.getAspects(targetNodeRef); + boolean found = false; + for (QName targetAspectTypeQName : targetAspects) + { + if (dictionaryService.isSubClass(targetAspectTypeQName, targetDef.getName())) + { + found = true; + break; + } + } + if (!found) + { + IntegrityRecord result = new IntegrityRecord( + "The association target is missing the aspect required for this association: \n" + + " Association: " + assocDef + "\n" + + " Required Target Aspect: " + targetDef.getName() + "\n" + + " Actual Target Aspects: " + targetAspects); + eventResults.add(result); + } + } + else + { + IntegrityRecord result = new IntegrityRecord( + "Unknown ClassDefinition subclass on the target definition: \n" + + " Association: " + assocDef + "\n" + + " Source Definition: " + targetDef.getName()); + eventResults.add(result); + } + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityChecker.java b/source/java/org/alfresco/repo/node/integrity/IntegrityChecker.java new file mode 100644 index 0000000000..20e2efce0d --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityChecker.java @@ -0,0 +1,643 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation of the {@link org.alfresco.repo.integrity.IntegrityService integrity service} + * that uses the domain persistence mechanism to store and recall integrity events. + *

    + * In order to fulfill the contract of the interface, this class registers to receive notifications + * pertinent to changes in the node structure. These are then store away in the persistent + * store until the request to + * {@link org.alfresco.repo.integrity.IntegrityService#checkIntegrity(String) check integrity} is + * made. + *

    + * In order to ensure registration of these events, the {@link #init()} method must be called. + *

    + * By default, this service is enabled, but can be disabled using {@link #setEnabled(boolean)}.
    + * Tracing of the event stacks is, for performance reasons, disabled by default but can be enabled + * using {@link #setTraceOn(boolean)}.
    + * When enabled, the integrity check can either fail with a RuntimeException or not. In either + * case, the integrity violations are logged as warnings or errors. This behaviour is controleed using + * {@link #setFailOnViolation(boolean)} and is off by default. In other words, if not set, this service + * will only log warnings about integrity violations. + *

    + * Some integrity checks are not performed here as they are dealt with directly during the modification + * operation in the {@link org.alfresco.service.cmr.repository.NodeService node service}. + * + * @see #setPolicyComponent(PolicyComponent) + * @see #setDictionaryService(DictionaryService) + * @see #setIntegrityDaoService(IntegrityDaoService) + * @see #setMaxErrorsPerTransaction(int) + * @see #setFlushSize(int) + * + * @author Derek Hulley + */ +public class IntegrityChecker + implements NodeServicePolicies.OnCreateNodePolicy, + NodeServicePolicies.OnUpdatePropertiesPolicy, + NodeServicePolicies.OnDeleteNodePolicy, + NodeServicePolicies.OnAddAspectPolicy, + NodeServicePolicies.OnRemoveAspectPolicy, + NodeServicePolicies.OnCreateChildAssociationPolicy, + NodeServicePolicies.OnDeleteChildAssociationPolicy, + NodeServicePolicies.OnCreateAssociationPolicy, + NodeServicePolicies.OnDeleteAssociationPolicy +{ + private static Log logger = LogFactory.getLog(IntegrityChecker.class); + + /** key against which the set of events is stored in the current transaction */ + private static final String KEY_EVENT_SET = "IntegrityChecker.EventSet"; + + private PolicyComponent policyComponent; + private DictionaryService dictionaryService; + private NodeService nodeService; + private boolean enabled; + private boolean failOnViolation; + private int maxErrorsPerTransaction; + private boolean traceOn; + + /** + */ + public IntegrityChecker() + { + this.enabled = true; + this.failOnViolation = false; + this.maxErrorsPerTransaction = 10; + this.traceOn = false; + } + + /** + * @param policyComponent the component to register behaviour with + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param dictionaryService the dictionary against which to confirm model details + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param nodeService the node service to use for browsing node structures + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param enabled set to false to disable integrity checking completely + */ + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + /** + * @param traceOn set to true to enable stack traces recording + * of events + */ + public void setTraceOn(boolean traceOn) + { + this.traceOn = traceOn; + } + + /** + * @param failOnViolation set to true to force failure by + * RuntimeException when a violation occurs. + */ + public void setFailOnViolation(boolean failOnViolation) + { + this.failOnViolation = failOnViolation; + } + + /** + * @param maxLogNumberPerTransaction upper limit on how many violations are + * logged when multiple violations have been found. + */ + public void setMaxErrorsPerTransaction(int maxLogNumberPerTransaction) + { + this.maxErrorsPerTransaction = maxLogNumberPerTransaction; + } + + /** + * Registers the system-level policy behaviours + */ + public void init() + { + // check that required properties have been set + if (dictionaryService == null) + throw new AlfrescoRuntimeException("IntegrityChecker property not set: dictionaryService"); + if (nodeService == null) + throw new AlfrescoRuntimeException("IntegrityChecker property not set: nodeService"); + if (policyComponent == null) + throw new AlfrescoRuntimeException("IntegrityChecker property not set: policyComponent"); + + if (enabled) // only register behaviour if integrity checking is on + { + // register behaviour + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), + this, + new JavaBehaviour(this, "onCreateNode")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + this, + new JavaBehaviour(this, "onUpdateProperties")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteNode"), + this, + new JavaBehaviour(this, "onDeleteNode")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + this, + new JavaBehaviour(this, "onAddAspect")); + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onRemoveAspect"), + this, + new JavaBehaviour(this, "onRemoveAspect")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + this, + new JavaBehaviour(this, "onCreateChildAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteChildAssociation"), + this, + new JavaBehaviour(this, "onDeleteChildAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateAssociation"), + this, + new JavaBehaviour(this, "onCreateAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteAssociation"), + this, + new JavaBehaviour(this, "onDeleteAssociation")); + } + } + + /** + * Ensures that this service is registered with the transaction and saves the event + * + * @param event + */ + @SuppressWarnings("unchecked") + private void save(IntegrityEvent event) + { + // optionally set trace + if (traceOn) + { + // get a stack trace + Throwable t = new Throwable(); + t.fillInStackTrace(); + StackTraceElement[] trace = t.getStackTrace(); + + event.addTrace(trace); + // done + } + + // register this service + AlfrescoTransactionSupport.bindIntegrityChecker(this); + + // get the event list + Map events = + (Map) AlfrescoTransactionSupport.getResource(KEY_EVENT_SET); + if (events == null) + { + events = new HashMap(113, 0.75F); + AlfrescoTransactionSupport.bindResource(KEY_EVENT_SET, events); + } + // check if the event is present + IntegrityEvent existingEvent = events.get(event); + if (existingEvent != null) + { + // the event (or its equivalent is already present - transfer the trace + if (traceOn) + { + existingEvent.getTraces().addAll(event.getTraces()); + } + } + else + { + // the event doesn't already exist + events.put(event, event); + } + if (logger.isDebugEnabled()) + { + logger.debug("" + (existingEvent != null ? "Event already present in" : "Added event to") + " event set: \n" + + " event: " + event); + } + } + + /** + * @see PropertiesIntegrityEvent + */ + public void onCreateNode(ChildAssociationRef childAssocRef) + { + IntegrityEvent event = null; + // check properties on child node + event = new PropertiesIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getChildRef()); + save(event); + + // check target role + event = new AssocTargetRoleIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getParentRef(), + childAssocRef.getTypeQName(), + childAssocRef.getQName()); + save(event); + + // check for associations defined on the new node (child) + NodeRef childRef = childAssocRef.getChildRef(); + QName childNodeTypeQName = nodeService.getType(childRef); + ClassDefinition nodeTypeDef = dictionaryService.getClass(childNodeTypeQName); + if (nodeTypeDef == null) + { + throw new DictionaryException("The node type is not recognized: " + childNodeTypeQName); + } + Map childAssocDefs = nodeTypeDef.getAssociations(); + + // check the multiplicity of each association with the node acting as a source + for (AssociationDefinition assocDef : childAssocDefs.values()) + { + QName assocTypeQName = assocDef.getName(); + // check target multiplicity + event = new AssocTargetMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + childRef, + assocTypeQName, + false); + save(event); + } + } + + /** + * @see PropertiesIntegrityEvent + */ + public void onUpdateProperties( + NodeRef nodeRef, + Map before, + Map after) + { + IntegrityEvent event = null; + // check properties on node + event = new PropertiesIntegrityEvent(nodeService, dictionaryService, nodeRef); + save(event); + } + + /** + * No checking performed: The association changes will be handled + */ + public void onDeleteNode(ChildAssociationRef childAssocRef) + { + } + + /** + * @see PropertiesIntegrityEvent + */ + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + IntegrityEvent event = null; + // check properties on node + event = new PropertiesIntegrityEvent(nodeService, dictionaryService, nodeRef); + save(event); + + // check for associations defined on the aspect + AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName); + if (aspectDef == null) + { + throw new DictionaryException("The aspect type is not recognized: " + aspectTypeQName); + } + Map assocDefs = aspectDef.getAssociations(); + + // check the multiplicity of each association with the node acting as a source + for (AssociationDefinition assocDef : assocDefs.values()) + { + QName assocTypeQName = assocDef.getName(); + // check target multiplicity + event = new AssocTargetMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + nodeRef, + assocTypeQName, + false); + save(event); + } + } + + /** + * No checking performed: The property changes will be handled + */ + public void onRemoveAspect(NodeRef nodeRef, QName aspectTypeQName) + { + } + + public void onCreateChildAssociation(ChildAssociationRef childAssocRef) + { + IntegrityEvent event = null; + // check source type + event = new AssocSourceTypeIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getParentRef(), + childAssocRef.getTypeQName()); + save(event); + // check target type + event = new AssocTargetTypeIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getChildRef(), + childAssocRef.getTypeQName()); + save(event); + // check source multiplicity + event = new AssocSourceMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getChildRef(), + childAssocRef.getTypeQName(), + false); + save(event); + // check target multiplicity + event = new AssocTargetMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getParentRef(), + childAssocRef.getTypeQName(), + false); + save(event); + // check target role + event = new AssocTargetRoleIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getParentRef(), + childAssocRef.getTypeQName(), + childAssocRef.getQName()); + save(event); + } + + /** + * @see CreateChildAssocIntegrityEvent + */ + public void onDeleteChildAssociation(ChildAssociationRef childAssocRef) + { + IntegrityEvent event = null; + // check source multiplicity + event = new AssocSourceMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getChildRef(), + childAssocRef.getTypeQName(), + true); + save(event); + // check target multiplicity + event = new AssocTargetMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + childAssocRef.getParentRef(), + childAssocRef.getTypeQName(), + true); + save(event); + } + + /** + * @see AbstractAssocIntegrityEvent + */ + public void onCreateAssociation(AssociationRef nodeAssocRef) + { + IntegrityEvent event = null; + // check source type + event = new AssocSourceTypeIntegrityEvent( + nodeService, + dictionaryService, + nodeAssocRef.getSourceRef(), + nodeAssocRef.getTypeQName()); + save(event); + // check target type + event = new AssocTargetTypeIntegrityEvent( + nodeService, + dictionaryService, + nodeAssocRef.getTargetRef(), + nodeAssocRef.getTypeQName()); + save(event); + // check source multiplicity + event = new AssocSourceMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + nodeAssocRef.getTargetRef(), + nodeAssocRef.getTypeQName(), + false); + save(event); + // check target multiplicity + event = new AssocTargetMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + nodeAssocRef.getSourceRef(), + nodeAssocRef.getTypeQName(), + false); + save(event); + } + + /** + * @see AbstractAssocIntegrityEvent + */ + public void onDeleteAssociation(AssociationRef nodeAssocRef) + { + IntegrityEvent event = null; + // check source multiplicity + event = new AssocSourceMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + nodeAssocRef.getTargetRef(), + nodeAssocRef.getTypeQName(), + true); + save(event); + // check target multiplicity + event = new AssocTargetMultiplicityIntegrityEvent( + nodeService, + dictionaryService, + nodeAssocRef.getSourceRef(), + nodeAssocRef.getTypeQName(), + true); + save(event); + } + + /** + * Runs several types of checks, querying specifically for events that + * will necessitate each type of test. + *

    + * The interface contracts also requires that all events for the transaction + * get cleaned up. + */ + public void checkIntegrity() throws IntegrityException + { + if (!enabled) + { + return; + } + + // process events and check for failures + List failures = processAllEvents(); + // clear out all events + AlfrescoTransactionSupport.unbindResource(KEY_EVENT_SET); + + // drop out quickly if there are no failures + if (failures.isEmpty()) + { + return; + } + + // handle errors according to instance flags + // firstly, log all failures + int failureCount = failures.size(); + StringBuilder sb = new StringBuilder(300 * failureCount); + sb.append("Found ").append(failureCount).append(" integrity violations"); + if (maxErrorsPerTransaction < failureCount) + { + sb.append(" - first ").append(maxErrorsPerTransaction); + } + sb.append(":"); + int count = 0; + for (IntegrityRecord failure : failures) + { + // break if we exceed the maximum number of log entries + count++; + if (count > maxErrorsPerTransaction) + { + break; + } + sb.append("\n").append(failure); + } + if (failOnViolation) + { + logger.error(sb.toString()); + throw new IntegrityException(failures); + } + else + { + logger.warn(sb.toString()); + // no exception + } + } + + /** + * Loops through all the integrity events and checks integrity. + *

    + * The events are stored in a set, so there are no duplicates. Since each + * event performs a particular type of check, this ensures that we don't + * duplicate checks. + * + * @return Returns a list of integrity violations, up to the + * {@link #maxErrorsPerTransaction the maximum defined} + */ + @SuppressWarnings("unchecked") + private List processAllEvents() + { + // the results + ArrayList allIntegrityResults = new ArrayList(0); // generally unused + + // get all the events for the transaction (or unit of work) + // duplicates have been elimiated + Map events = + (Map) AlfrescoTransactionSupport.getResource(KEY_EVENT_SET); + if (events == null) + { + // no events were registered - nothing of significance happened + return allIntegrityResults; + } + + // failure results for the event + List integrityRecords = new ArrayList(0); + + // cycle through the events, performing checking integrity + for (IntegrityEvent event : events.keySet()) + { + try + { + event.checkIntegrity(integrityRecords); + } + catch (Throwable e) + { + e.printStackTrace(); + // log it as an error and move to next event + IntegrityRecord exceptionRecord = new IntegrityRecord("" + e.getMessage()); + exceptionRecord.setTraces(Collections.singletonList(e.getStackTrace())); + allIntegrityResults.add(exceptionRecord); + // move on + continue; + } + + // keep track of results needing trace added + if (traceOn) + { + // record the current event trace if present + for (IntegrityRecord integrityRecord : integrityRecords) + { + integrityRecord.setTraces(event.getTraces()); + } + } + + // copy all the event results to the final results + allIntegrityResults.addAll(integrityRecords); + // clear the event results + integrityRecords.clear(); + + if (allIntegrityResults.size() >= maxErrorsPerTransaction) + { + // only so many errors wanted at a time + break; + } + } + // done + return allIntegrityResults; + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/IntegrityEvent.java new file mode 100644 index 0000000000..25ca6ea213 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityEvent.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; + +/** + * Stores information for all events in the system + * + * @author Derek Hulley + */ +public interface IntegrityEvent +{ + /** + * Checks integrity pertinent to the event + * + * @param eventResults the list of event results that can be added to + */ + public void checkIntegrity(List eventResults); + + public List getTraces(); + + public void addTrace(StackTraceElement[] trace); +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityEventTest.java b/source/java/org/alfresco/repo/node/integrity/IntegrityEventTest.java new file mode 100644 index 0000000000..518ebb7b26 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityEventTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * @see org.alfresco.repo.node.integrity.IntegrityEvent + * + * @author Derek Hulley + */ +public class IntegrityEventTest extends TestCase +{ + private static final String NAMESPACE = "http://test"; + + private NodeRef nodeRef; + private QName typeQName; + private QName qname; + private IntegrityEvent event; + + public void setUp() throws Exception + { + nodeRef = new NodeRef("workspace://protocol/ID123"); + typeQName = QName.createQName(NAMESPACE, "SomeTypeQName"); + qname = QName.createQName(NAMESPACE, "qname"); + + event = new TestIntegrityEvent(null, null, nodeRef, typeQName, qname); + } + + public void testSetFunctionality() throws Exception + { + Set set = new HashSet(5); + boolean added = set.add(event); + assertTrue(added); + added = set.add(new TestIntegrityEvent(null, null, nodeRef, typeQName, qname)); + assertFalse(added); + } + + private static class TestIntegrityEvent extends AbstractIntegrityEvent + { + public TestIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef nodeRef, + QName typeQName, + QName qname) + { + super(nodeService, dictionaryService, nodeRef, typeQName, qname); + } + + public void checkIntegrity(List eventResults) + { + throw new UnsupportedOperationException(); + } + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityException.java b/source/java/org/alfresco/repo/node/integrity/IntegrityException.java new file mode 100644 index 0000000000..9f42e8e1fc --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Thrown when an integrity check fails + * + * @author Derek Hulley + */ +public class IntegrityException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = -5036557255854195669L; + + private List records; + + public IntegrityException(List records) + { + super("Integrity failure"); + this.records = records; + } + + /** + * @return Returns a list of all the integrity violations + */ + public List getRecords() + { + return records; + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityRecord.java b/source/java/org/alfresco/repo/node/integrity/IntegrityRecord.java new file mode 100644 index 0000000000..4927518762 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityRecord.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.util.List; + +/** + * Represents an integrity violation + * + * @author Derek Hulley + */ +public class IntegrityRecord +{ + private String msg; + private List traces; + + /** + * @param msg the violation message + */ + public IntegrityRecord(String msg) + { + this.msg = msg; + this.traces = null; + } + + /** + * Add a stack trace to the list of traces associated with this failure + * + * @param trace a stack trace + */ + public void setTraces(List traces) + { + this.traces = traces; + } + + public String getMessage() + { + return msg; + } + + /** + * Dumps the integrity message and, if present, the stack trace + */ + public String toString() + { + StringBuilder sb = new StringBuilder(msg.length() * 2); + if (traces == null) + { + sb.append(msg); + } + else + { + sb.append(msg); + for (StackTraceElement[] trace : traces) + { + sb.append("\n Trace of possible cause:"); + for (int i = 0; i < trace.length; i++) + { + sb.append("\n ").append(trace[i]); + } + } + } + return sb.toString(); + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java b/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java new file mode 100644 index 0000000000..cca1156512 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.io.InputStream; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; + +/** + * Attempts to build faulty node structures in order to test integrity. + *

    + * The entire application context is loaded as is, but the integrity fail- + * mode is set to throw an exception. + * + * TODO: Role name restrictions must be checked + * + * @author Derek Hulley + */ +public class IntegrityTest extends TestCase +{ + private static Log logger = LogFactory.getLog(IntegrityTest.class); + + public static final String NAMESPACE = "http://www.alfresco.org/test/IntegrityTest"; + public static final String TEST_PREFIX = "test"; + + public static final QName TEST_TYPE_WITHOUT_ANYTHING = QName.createQName(NAMESPACE, "typeWithoutAnything"); + public static final QName TEST_TYPE_WITH_ASPECT = QName.createQName(NAMESPACE, "typeWithAspect"); + public static final QName TEST_TYPE_WITH_PROPERTIES = QName.createQName(NAMESPACE, "typeWithProperties"); + public static final QName TEST_TYPE_WITH_ASSOCS = QName.createQName(NAMESPACE, "typeWithAssocs"); + public static final QName TEST_TYPE_WITH_CHILD_ASSOCS = QName.createQName(NAMESPACE, "typeWithChildAssocs"); + + public static final QName TEST_ASSOC_NODE_ZEROMANY_ZEROMANY = QName.createQName(NAMESPACE, "assoc-0to* - 0to*"); + public static final QName TEST_ASSOC_CHILD_ZEROMANY_ZEROMANY = QName.createQName(NAMESPACE, "child-0to* - 0to*"); + public static final QName TEST_ASSOC_NODE_ONE_ONE = QName.createQName(NAMESPACE, "assoc-1to1 - 1to1"); + public static final QName TEST_ASSOC_CHILD_ONE_ONE = QName.createQName(NAMESPACE, "child-1to1 - 1to1"); + public static final QName TEST_ASSOC_ASPECT_ONE_ONE = QName.createQName(NAMESPACE, "aspect-assoc-1to1 - 1to1"); + + public static final QName TEST_ASPECT_WITH_PROPERTIES = QName.createQName(NAMESPACE, "aspectWithProperties"); + public static final QName TEST_ASPECT_WITH_ASSOC = QName.createQName(NAMESPACE, "aspectWithAssoc"); + + public static final QName TEST_PROP_TEXT_A = QName.createQName(NAMESPACE, "prop-text-a"); + public static final QName TEST_PROP_TEXT_B = QName.createQName(NAMESPACE, "prop-text-b"); + public static final QName TEST_PROP_INT_A = QName.createQName(NAMESPACE, "prop-int-a"); + public static final QName TEST_PROP_INT_B = QName.createQName(NAMESPACE, "prop-int-b"); + + private static ApplicationContext ctx; + static + { + ctx = ApplicationContextHelper.getApplicationContext(); + } + + private IntegrityChecker integrityChecker; + private ServiceRegistry serviceRegistry; + private NodeService nodeService; + private NodeRef rootNodeRef; + private PropertyMap allProperties; + private UserTransaction txn; + private AuthenticationComponent authenticationComponent; + + public void setUp() throws Exception + { + DictionaryDAO dictionaryDao = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + ClassLoader cl = BaseNodeServiceTest.class.getClassLoader(); + // load the test model + InputStream modelStream = cl.getResourceAsStream("org/alfresco/repo/node/integrity/IntegrityTest_model.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + integrityChecker = (IntegrityChecker) ctx.getBean("integrityChecker"); + integrityChecker.setEnabled(true); + integrityChecker.setFailOnViolation(true); + integrityChecker.setTraceOn(true); + integrityChecker.setMaxErrorsPerTransaction(100); // we want to count the correct number of errors + + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + nodeService = serviceRegistry.getNodeService(); + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + // begin a transaction + TransactionService transactionService = serviceRegistry.getTransactionService(); + txn = transactionService.getUserTransaction(); + txn.begin(); + StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, getName()); + if (!nodeService.exists(storeRef)) + { + nodeService.createStore(storeRef.getProtocol(), storeRef.getIdentifier()); + } + rootNodeRef = nodeService.getRootNode(storeRef); + + allProperties = new PropertyMap(); + allProperties.put(TEST_PROP_TEXT_A, "ABC"); + allProperties.put(TEST_PROP_TEXT_B, "DEF"); + allProperties.put(TEST_PROP_INT_A, "123"); + allProperties.put(TEST_PROP_INT_B, "456"); + } + + public void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + txn.rollback(); + } + + /** + * Create a node of the given type, and hanging off the root node + */ + private NodeRef createNode(String name, QName type, PropertyMap properties) + { + return nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NAMESPACE, name), + type, + properties + ).getChildRef(); + } + + private void checkIntegrityNoFailure() throws Exception + { + integrityChecker.checkIntegrity(); + } + + /** + * + * @param failureMsg the fail message if an integrity exception doesn't occur + * @param expectedCount the expected number of integrity failures, or -1 to ignore + */ + private void checkIntegrityExpectFailure(String failureMsg, int expectedCount) + { + try + { + integrityChecker.checkIntegrity(); + fail(failureMsg); + } + catch (IntegrityException e) + { + if (expectedCount >= 0) + { + assertEquals("Incorrect number of integrity records generated", expectedCount, e.getRecords().size()); + } + } + } + + public void testSetUp() throws Exception + { + assertNotNull("Static IntegrityChecker not created", integrityChecker); + } + + public void testCreateWithoutProperties() throws Exception + { + NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_PROPERTIES, null); + checkIntegrityExpectFailure("Failed to detect missing properties", 1); + } + + public void testCreateWithProperties() throws Exception + { + NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_PROPERTIES, allProperties); + checkIntegrityNoFailure(); + } + + public void testMandatoryPropertiesRemoved() throws Exception + { + NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_PROPERTIES, allProperties); + + // remove all the properties + PropertyMap properties = new PropertyMap(); + nodeService.setProperties(nodeRef, properties); + + checkIntegrityExpectFailure("Failed to detect missing removed properties", 1); + } + + public void testCreateWithoutPropertiesForAspect() throws Exception + { + NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_ASPECT, null); + + checkIntegrityExpectFailure("Failed to detect missing properties for aspect", 1); + } + + public void testCreateWithPropertiesForAspect() throws Exception + { + NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_ASPECT, allProperties); + checkIntegrityNoFailure(); + } + + public void testCreateTargetOfAssocsWithMandatorySourcesPresent() throws Exception + { + // this is the target of 3 assoc types where the source cardinality is 1..1 + NodeRef targetAndChild = createNode("targetAndChild", TEST_TYPE_WITHOUT_ANYTHING, null); + + NodeRef source = createNode("source", TEST_TYPE_WITH_ASSOCS, null); + nodeService.createAssociation(source, targetAndChild, TEST_ASSOC_NODE_ONE_ONE); + + NodeRef parent = createNode("parent", TEST_TYPE_WITH_CHILD_ASSOCS, null); + nodeService.addChild(parent, targetAndChild, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "mandatoryChild")); + + NodeRef aspected = createNode("aspectNode", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.addAspect(aspected, TEST_ASPECT_WITH_ASSOC, null); + nodeService.createAssociation(aspected, targetAndChild, TEST_ASSOC_ASPECT_ONE_ONE); + + checkIntegrityNoFailure(); + } + + /** + * TODO: The dictionary support for the reverse lookup of mandatory associations will + * allow this method to go in + *

    + * Does nothing. + */ + public void testCreateTargetOfAssocsWithMandatorySourcesMissing() throws Exception + { +// // this is the target of 3 associations where the source cardinality is 1..1 +// NodeRef target = createNode("abc", TEST_TYPE_WITHOUT_ANYTHING, null); +// +// checkIntegrityExpectFailure("Failed to detect missing mandatory assoc sources", 3); + logger.error("Method commented out: testCreateTargetOfAssocsWithMandatorySourcesMissing"); + } + + /** + * TODO: Reactivate once cascade delete notifications are back on + *

    + * Does nothing. + */ + public void testRemoveSourcesOfMandatoryAssocs() throws Exception + { +// // this is the target of 3 assoc types where the source cardinality is 1..1 +// NodeRef targetAndChild = createNode("targetAndChild", TEST_TYPE_WITHOUT_ANYTHING, null); +// +// NodeRef source = createNode("source", TEST_TYPE_WITH_ASSOCS, null); +// nodeService.createAssociation(source, targetAndChild, TEST_ASSOC_NODE_ONE_ONE); +// +// NodeRef parent = createNode("parent", TEST_TYPE_WITH_CHILD_ASSOCS, null); +// nodeService.addChild(parent, targetAndChild, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "mandatoryChild")); +// +// NodeRef aspectSource = createNode("aspectSource", TEST_TYPE_WITHOUT_ANYTHING, null); +// nodeService.addAspect(aspectSource, TEST_ASPECT_WITH_ASSOC, null); +// nodeService.createAssociation(aspectSource, targetAndChild, TEST_ASSOC_ASPECT_ONE_ONE); +// +// checkIntegrityNoFailure(); +// +// // remove source nodes +// nodeService.deleteNode(source); +// nodeService.deleteNode(parent); +// nodeService.deleteNode(aspectSource); +// +// checkIntegrityExpectFailure("Failed to detect removal of mandatory assoc sources", 3); + logger.error("Method commented out: testRemoveSourcesOfMandatoryAssocs"); + } + + public void testDuplicateTargetAssocs() throws Exception + { + NodeRef parent = createNode("source", TEST_TYPE_WITH_CHILD_ASSOCS, null); + NodeRef child1 = createNode("child1", TEST_TYPE_WITHOUT_ANYTHING, null); + NodeRef child2 = createNode("child2", TEST_TYPE_WITHOUT_ANYTHING, null); + NodeRef child3 = createNode("child3", TEST_TYPE_WITHOUT_ANYTHING, null); + + // satisfy the one-to-one + nodeService.addChild(parent, child3, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "mandatoryChild")); + + // create the non-duplicate assocs + nodeService.addChild(parent, child1, TEST_ASSOC_CHILD_ZEROMANY_ZEROMANY, QName.createQName(NAMESPACE, "dupli_cate")); + nodeService.addChild(parent, child2, TEST_ASSOC_CHILD_ZEROMANY_ZEROMANY, QName.createQName(NAMESPACE, "dupli_cate")); + + checkIntegrityExpectFailure("Failed to detect duplicate association names", 1); + } + + public void testCreateSourceOfAssocsWithMandatoryTargetsPresent() throws Exception + { + NodeRef source = createNode("abc", TEST_TYPE_WITH_ASSOCS, null); + NodeRef target = createNode("target", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.createAssociation(source, target, TEST_ASSOC_NODE_ONE_ONE); + + NodeRef parent = createNode("parent", TEST_TYPE_WITH_CHILD_ASSOCS, null); + NodeRef child = createNode("child", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.addChild(parent, child, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "one-to-one")); + + NodeRef aspectSource = createNode("aspectSource", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.addAspect(aspectSource, TEST_ASPECT_WITH_ASSOC, null); + NodeRef aspectTarget = createNode("aspectTarget", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.createAssociation(aspectSource, aspectTarget, TEST_ASSOC_ASPECT_ONE_ONE); + + checkIntegrityNoFailure(); + } + + public void testCreateSourceOfAssocsWithMandatoryTargetsMissing() throws Exception + { + NodeRef source = createNode("abc", TEST_TYPE_WITH_ASSOCS, null); + + NodeRef parent = createNode("parent", TEST_TYPE_WITH_CHILD_ASSOCS, null); + + NodeRef aspectSource = createNode("aspectSource", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.addAspect(aspectSource, TEST_ASPECT_WITH_ASSOC, null); + + checkIntegrityExpectFailure("Failed to detect missing assoc targets", 3); + } + + /** + * TODO: Reactivate once cascade delete notifications are back on + *

    + * Does nothing. + */ + public void testRemoveTargetsOfMandatoryAssocs() throws Exception + { +// NodeRef source = createNode("abc", TEST_TYPE_WITH_ASSOCS, null); +// NodeRef target = createNode("target", TEST_TYPE_WITHOUT_ANYTHING, null); +// nodeService.createAssociation(source, target, TEST_ASSOC_NODE_ONE_ONE); +// +// NodeRef parent = createNode("parent", TEST_TYPE_WITH_CHILD_ASSOCS, null); +// NodeRef child = createNode("child", TEST_TYPE_WITHOUT_ANYTHING, null); +// nodeService.addChild(parent, child, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "one-to-one")); +// +// NodeRef aspectSource = createNode("aspectSource", TEST_TYPE_WITHOUT_ANYTHING, null); +// nodeService.addAspect(aspectSource, TEST_ASPECT_WITH_ASSOC, null); +// NodeRef aspectTarget = createNode("aspectTarget", TEST_TYPE_WITHOUT_ANYTHING, null); +// nodeService.createAssociation(aspectSource, aspectTarget, TEST_ASSOC_ASPECT_ONE_ONE); +// +// checkIntegrityNoFailure(); +// +// // remove target nodes +// nodeService.deleteNode(target); +// nodeService.deleteNode(child); +// nodeService.deleteNode(aspectTarget); +// +// checkIntegrityExpectFailure("Failed to detect removal of mandatory assoc targets", 3); + logger.error("Method commented out: testRemoveTargetsOfMandatoryAssocs"); + } + + public void testExcessTargetsOfOneToOneAssocs() throws Exception + { + NodeRef source = createNode("abc", TEST_TYPE_WITH_ASSOCS, null); + NodeRef target1 = createNode("target1", TEST_TYPE_WITHOUT_ANYTHING, null); + NodeRef target2 = createNode("target2", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.createAssociation(source, target1, TEST_ASSOC_NODE_ONE_ONE); + nodeService.createAssociation(source, target2, TEST_ASSOC_NODE_ONE_ONE); + + NodeRef parent = createNode("parent", TEST_TYPE_WITH_CHILD_ASSOCS, null); + NodeRef child1 = createNode("child1", TEST_TYPE_WITHOUT_ANYTHING, null); + NodeRef child2 = createNode("child2", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.addChild(parent, child1, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "one-to-one-first")); + nodeService.addChild(parent, child2, TEST_ASSOC_CHILD_ONE_ONE, QName.createQName(NAMESPACE, "one-to-one-second")); + + NodeRef aspectSource = createNode("aspectSource", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.addAspect(aspectSource, TEST_ASPECT_WITH_ASSOC, null); + NodeRef aspectTarget1 = createNode("aspectTarget1", TEST_TYPE_WITHOUT_ANYTHING, null); + NodeRef aspectTarget2 = createNode("aspectTarget2", TEST_TYPE_WITHOUT_ANYTHING, null); + nodeService.createAssociation(aspectSource, aspectTarget1, TEST_ASSOC_ASPECT_ONE_ONE); + nodeService.createAssociation(aspectSource, aspectTarget2, TEST_ASSOC_ASPECT_ONE_ONE); + + checkIntegrityExpectFailure("Failed to detect excess target cardinality for one-to-one assocs", 3); + } +} diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml b/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml new file mode 100644 index 0000000000..3ddf0c8dec --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml @@ -0,0 +1,140 @@ + + + Test Model for Integrity tests + Alfresco + 2005-06-05 + 0.1 + + + + + + + + + + + + + + Type Without Anything + sys:base + + + + Type With Properties + sys:base + + + d:text + true + + + d:text + + + + + + Type With Aspect + sys:base + + test:aspectWithProperties + + + + + Type With Assocs + sys:base + + + + false + true + + + test:typeWithoutAnything + false + true + + + + + true + false + + + test:typeWithoutAnything + true + false + + + + + + + Type With Child Assocs + sys:base + + + + false + true + + + test:typeWithoutAnything + false + true + + false + + + + true + false + + + test:typeWithoutAnything + true + false + + false + + + + + + + + + Aspect with Properties + + + d:int + true + + + d:int + + + + + + Aspect with associations + + + + true + false + + + test:typeWithoutAnything + true + false + + + + + + + diff --git a/source/java/org/alfresco/repo/node/integrity/PropertiesIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/PropertiesIntegrityEvent.java new file mode 100644 index 0000000000..65fcb46bf8 --- /dev/null +++ b/source/java/org/alfresco/repo/node/integrity/PropertiesIntegrityEvent.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.integrity; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Event raised to check nodes + * + * @author Derek Hulley + */ +public class PropertiesIntegrityEvent extends AbstractIntegrityEvent +{ + private static Log logger = LogFactory.getLog(PropertiesIntegrityEvent.class); + + protected PropertiesIntegrityEvent( + NodeService nodeService, + DictionaryService dictionaryService, + NodeRef nodeRef) + { + super(nodeService, dictionaryService, nodeRef, null, null); + } + + public void checkIntegrity(List eventResults) + { + try + { + checkAllProperties(getNodeRef(), eventResults); + } + catch (InvalidNodeRefException e) + { + // node has gone + if (logger.isDebugEnabled()) + { + logger.debug("Event ignored - node gone: " + this); + } + eventResults.clear(); + return; + } + } + + /** + * Checks the properties for the type and aspects of the given node. + * + * @param nodeRef + * @param eventResults + */ + private void checkAllProperties(NodeRef nodeRef, List eventResults) + { + // get all properties for the node + Map nodeProperties = nodeService.getProperties(nodeRef); + + // get the node type + QName nodeTypeQName = nodeService.getType(nodeRef); + // get property definitions for the node type + TypeDefinition typeDef = dictionaryService.getType(nodeTypeQName); + Collection propertyDefs = typeDef.getProperties().values(); + // check them + checkAllProperties(nodeRef, nodeTypeQName, propertyDefs, nodeProperties, eventResults); + + // get the node aspects + Set aspectTypeQNames = nodeService.getAspects(nodeRef); + for (QName aspectTypeQName : aspectTypeQNames) + { + // get property definitions for the aspect + AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName); + propertyDefs = aspectDef.getProperties().values(); + // check them + checkAllProperties(nodeRef, aspectTypeQName, propertyDefs, nodeProperties, eventResults); + } + // done + } + + /** + * Checks the specific map of properties against the required property definitions + * + * @param nodeRef the node to which this applies + * @param typeQName the qualified name of the aspect or type to which the properties belong + * @param propertyDefs the definitions to check against - may be null or empty + * @param nodeProperties the properties to check + */ + private void checkAllProperties( + NodeRef nodeRef, + QName typeQName, + Collection propertyDefs, + Map nodeProperties, + Collection eventResults) + { + // check for null or empty definitions + if (propertyDefs == null || propertyDefs.isEmpty()) + { + return; + } + for (PropertyDefinition propertyDef : propertyDefs) + { + QName propertyQName = propertyDef.getName(); + Serializable propertyValue = nodeProperties.get(propertyQName); + // check that mandatory properties are set + if (propertyDef.isMandatory() && !nodeProperties.containsKey(propertyQName)) + { + IntegrityRecord result = new IntegrityRecord( + "Mandatory property not set: \n" + + " Node: " + nodeRef + "\n" + + " Type: " + typeQName + "\n" + + " Property: " + propertyQName); + eventResults.add(result); + // next one + continue; + } + // TODO: Incorporate value constraint checks - JIRA AR166 + } + } +} diff --git a/source/java/org/alfresco/repo/ownable/impl/OwnableServiceImpl.java b/source/java/org/alfresco/repo/ownable/impl/OwnableServiceImpl.java new file mode 100644 index 0000000000..7e1c10fcd5 --- /dev/null +++ b/source/java/org/alfresco/repo/ownable/impl/OwnableServiceImpl.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.ownable.impl; + +import java.io.Serializable; +import java.util.HashMap; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.namespace.QName; +import org.springframework.beans.factory.InitializingBean; + +public class OwnableServiceImpl implements OwnableService, InitializingBean +{ + private NodeService nodeService; + + private AuthenticationService authenticationService; + + public OwnableServiceImpl() + { + super(); + } + + // IOC + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + + public void afterPropertiesSet() throws Exception + { + if(nodeService == null) + { + throw new IllegalArgumentException("A node service must be set"); + } + if(authenticationService == null) + { + throw new IllegalArgumentException("An authentication service must be set"); + } + } + + // OwnableService implmentation + + + public String getOwner(NodeRef nodeRef) + { + String userName = null; + // If ownership is not explicitly set then we fall back to the creator + // + if(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_OWNABLE)) + { + userName = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, ContentModel.PROP_OWNER)); + } + else if(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_AUDITABLE)) + { + userName = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, ContentModel.PROP_CREATOR)); + } + return userName; + } + + public void setOwner(NodeRef nodeRef, String userName) + { + if(!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_OWNABLE)) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_OWNER, userName); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_OWNABLE, properties); + } + else + { + nodeService.setProperty(nodeRef, ContentModel.PROP_OWNER, userName); + } + + } + + public void takeOwnership(NodeRef nodeRef) + { + setOwner(nodeRef, authenticationService.getCurrentUserName()); + } + + public boolean hasOwner(NodeRef nodeRef) + { + return getOwner(nodeRef) != null; + } + +} diff --git a/source/java/org/alfresco/repo/ownable/impl/OwnableServiceNOOPImpl.java b/source/java/org/alfresco/repo/ownable/impl/OwnableServiceNOOPImpl.java new file mode 100644 index 0000000000..542fc6d6ef --- /dev/null +++ b/source/java/org/alfresco/repo/ownable/impl/OwnableServiceNOOPImpl.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.ownable.impl; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.OwnableService; + +/** + * A simple implementation that does not support ownership. + * + * @author Andy Hind + */ +public class OwnableServiceNOOPImpl implements OwnableService +{ + + public OwnableServiceNOOPImpl() + { + super(); + } + + public String getOwner(NodeRef nodeRef) + { + // Return null as there is no owner. + return null; + } + + public void setOwner(NodeRef nodeRef, String userName) + { + // No action. + } + + public void takeOwnership(NodeRef nodeRef) + { + // No action. + } + + public boolean hasOwner(NodeRef nodeRef) + { + // There is no owner for any node. + return false; + } + +} diff --git a/source/java/org/alfresco/repo/ownable/impl/OwnableServiceTest.java b/source/java/org/alfresco/repo/ownable/impl/OwnableServiceTest.java new file mode 100644 index 0000000000..0f42889056 --- /dev/null +++ b/source/java/org/alfresco/repo/ownable/impl/OwnableServiceTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.ownable.impl; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.MutableAuthenticationDao; +import org.alfresco.repo.security.permissions.dynamic.OwnerDynamicAuthority; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class OwnableServiceTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private NodeService nodeService; + + private AuthenticationService authenticationService; + + private AuthenticationComponent authenticationComponent; + + private MutableAuthenticationDao authenticationDAO; + + private OwnableService ownableService; + + private NodeRef rootNodeRef; + + private UserTransaction userTransaction; + + private PermissionService permissionService; + + private OwnerDynamicAuthority dynamicAuthority; + + public OwnableServiceTest() + { + super(); + } + + public OwnableServiceTest(String arg0) + { + super(arg0); + } + + public void setUp() throws Exception + { + nodeService = (NodeService) ctx.getBean("nodeService"); + authenticationService = (AuthenticationService) ctx.getBean("authenticationService"); + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + ownableService = (OwnableService) ctx.getBean("ownableService"); + permissionService = (PermissionService) ctx.getBean("permissionService"); + + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + authenticationDAO = (MutableAuthenticationDao) ctx.getBean("alfDaoImpl"); + + + TransactionService transactionService = (TransactionService) ctx.getBean(ServiceRegistry.TRANSACTION_SERVICE.getLocalName()); + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + permissionService.setPermission(rootNodeRef, PermissionService.ALL_AUTHORITIES, PermissionService.ADD_CHILDREN, true); + + if(authenticationDAO.userExists("andy")) + { + authenticationService.deleteAuthentication("andy"); + } + authenticationService.createAuthentication("andy", "andy".toCharArray()); + + dynamicAuthority = new OwnerDynamicAuthority(); + dynamicAuthority.setOwnableService(ownableService); + + authenticationComponent.clearCurrentSecurityContext(); + } + + @Override + protected void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + userTransaction.rollback(); + super.tearDown(); + } + + public void testSetup() + { + assertNotNull(nodeService); + assertNotNull(authenticationService); + assertNotNull(ownableService); + } + + public void testUnSet() + { + assertNull(ownableService.getOwner(rootNodeRef)); + assertFalse(ownableService.hasOwner(rootNodeRef)); + } + + public void testCMObject() + { + authenticationService.authenticate("andy", "andy".toCharArray()); + NodeRef testNode = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_PERSON, ContentModel.TYPE_CMOBJECT, null).getChildRef(); + permissionService.setPermission(rootNodeRef, "andy", PermissionService.TAKE_OWNERSHIP, true); + assertEquals("andy", ownableService.getOwner(testNode)); + assertTrue(ownableService.hasOwner(testNode)); + assertTrue(nodeService.hasAspect(testNode, ContentModel.ASPECT_AUDITABLE)); + assertFalse(nodeService.hasAspect(testNode, ContentModel.ASPECT_OWNABLE)); + assertTrue(dynamicAuthority.hasAuthority(testNode, "andy")); + + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rootNodeRef, PermissionService.TAKE_OWNERSHIP)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rootNodeRef, PermissionService.SET_OWNER)); + + ownableService.setOwner(testNode, "muppet"); + assertEquals("muppet", ownableService.getOwner(testNode)); + ownableService.takeOwnership(testNode); + assertEquals("andy", ownableService.getOwner(testNode)); + assertTrue(nodeService.hasAspect(testNode, ContentModel.ASPECT_AUDITABLE)); + assertTrue(nodeService.hasAspect(testNode, ContentModel.ASPECT_OWNABLE)); + assertTrue(dynamicAuthority.hasAuthority(testNode, "andy")); + } + + public void testContainer() + { + authenticationService.authenticate("andy", "andy".toCharArray()); + NodeRef testNode = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_PERSON, ContentModel.TYPE_CONTAINER, null).getChildRef(); + assertNull(ownableService.getOwner(testNode)); + assertFalse(ownableService.hasOwner(testNode)); + assertFalse(nodeService.hasAspect(testNode, ContentModel.ASPECT_AUDITABLE)); + assertFalse(nodeService.hasAspect(testNode, ContentModel.ASPECT_OWNABLE)); + assertFalse(dynamicAuthority.hasAuthority(testNode, "andy")); + + assertFalse(permissionService.hasPermission(testNode, PermissionService.READ) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(testNode, permissionService.getAllPermission()) == AccessStatus.ALLOWED); + + permissionService.setPermission(rootNodeRef, permissionService.getOwnerAuthority(), permissionService.getAllPermission(), true); + + ownableService.setOwner(testNode, "muppet"); + assertEquals("muppet", ownableService.getOwner(testNode)); + ownableService.takeOwnership(testNode); + assertEquals("andy", ownableService.getOwner(testNode)); + assertFalse(nodeService.hasAspect(testNode, ContentModel.ASPECT_AUDITABLE)); + assertTrue(nodeService.hasAspect(testNode, ContentModel.ASPECT_OWNABLE)); + assertTrue(dynamicAuthority.hasAuthority(testNode, "andy")); + + assertTrue(permissionService.hasPermission(testNode, PermissionService.READ) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(testNode, permissionService.getAllPermission())== AccessStatus.ALLOWED); + + + } + +} diff --git a/source/java/org/alfresco/repo/policy/AssociationPolicy.java b/source/java/org/alfresco/repo/policy/AssociationPolicy.java new file mode 100644 index 0000000000..8297ace950 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/AssociationPolicy.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * Marker interface for representing an Association-level Policy. + * + * @author David Caruana + */ +public interface AssociationPolicy extends Policy +{ + +} diff --git a/source/java/org/alfresco/repo/policy/AssociationPolicyDelegate.java b/source/java/org/alfresco/repo/policy/AssociationPolicyDelegate.java new file mode 100644 index 0000000000..f80c8c54b9 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/AssociationPolicyDelegate.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Delegate for a Class Feature-level (Property and Association) Policies. Provides + * access to Policy Interface implementations which invoke the appropriate bound behaviours. + * + * @author David Caruana + * + * @param

    the policy interface + */ +public class AssociationPolicyDelegate

    +{ + private DictionaryService dictionary; + private CachedPolicyFactory factory; + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param policyClass the policy interface class + * @param index the behaviour index to query against + */ + @SuppressWarnings("unchecked") + /*package*/ AssociationPolicyDelegate(DictionaryService dictionary, Class

    policyClass, BehaviourIndex index) + { + // Get list of all pre-registered behaviours for the policy and + // ensure they are valid. + Collection definitions = index.getAll(); + for (BehaviourDefinition definition : definitions) + { + definition.getBehaviour().getInterface(policyClass); + } + + // Rely on cached implementation of policy factory + // Note: Could also use PolicyFactory (without caching) + this.factory = new CachedPolicyFactory(policyClass, index); + this.dictionary = dictionary; + } + + /** + * Ensures the validity of the given assoc type + * + * @param assocTypeQName + * @throws IllegalArgumentException + */ + private void checkAssocType(QName assocTypeQName) throws IllegalArgumentException + { + AssociationDefinition assocDef = dictionary.getAssociation(assocTypeQName); + if (assocDef == null) + { + throw new IllegalArgumentException("Association " + assocTypeQName + " has not been defined in the data dictionary"); + } + } + + /** + * Gets the Policy implementation for the specified Class and Association + * + * When multiple behaviours are bound to the policy for the class feature, an + * aggregate policy implementation is returned which invokes each policy + * in turn. + * + * @param classQName the class qualified name + * @param assocTypeQName the association type qualified name + * @return the policy + */ + public P get(QName classQName, QName assocTypeQName) + { + return get(null, classQName, assocTypeQName); + } + + /** + * Gets the Policy implementation for the specified Class and Association + * + * When multiple behaviours are bound to the policy for the class feature, an + * aggregate policy implementation is returned which invokes each policy + * in turn. + * + * @param nodeRef the node reference + * @param classQName the class qualified name + * @param assocTypeQName the association type qualified name + * @return the policy + */ + public P get(NodeRef nodeRef, QName classQName, QName assocTypeQName) + { + checkAssocType(assocTypeQName); + return factory.create(new ClassFeatureBehaviourBinding(dictionary, nodeRef, classQName, assocTypeQName)); + } + + /** + * Gets the collection of Policy implementations for the specified Class and Association + * + * @param classQName the class qualified name + * @param assocTypeQName the association type qualified name + * @return the collection of policies + */ + public Collection

    getList(QName classQName, QName assocTypeQName) + { + return getList(null, classQName, assocTypeQName); + } + + /** + * Gets the collection of Policy implementations for the specified Class and Association + * + * @param nodeRef the node reference + * @param classQName the class qualified name + * @param assocTypeQName the association type qualified name + * @return the collection of policies + */ + public Collection

    getList(NodeRef nodeRef, QName classQName, QName assocTypeQName) + { + checkAssocType(assocTypeQName); + return factory.createList(new ClassFeatureBehaviourBinding(dictionary, nodeRef, classQName, assocTypeQName)); + } + + /** + * Gets a Policy for all the given Class and Association + * + * @param classQNames the class qualified names + * @param assocTypeQName the association type qualified name + * @return Return the policy + */ + public P get(Set classQNames, QName assocTypeQName) + { + return get(null, classQNames, assocTypeQName); + } + + /** + * Gets a Policy for all the given Class and Association + * + * @param nodeRef the node reference + * @param classQNames the class qualified names + * @param assocTypeQName the association type qualified name + * @return Return the policy + */ + public P get(NodeRef nodeRef, Set classQNames, QName assocTypeQName) + { + checkAssocType(assocTypeQName); + return factory.toPolicy(getList(nodeRef, classQNames, assocTypeQName)); + } + + /** + * Gets the Policy instances for all the given Classes and Associations + * + * @param classQNames the class qualified names + * @param assocTypeQName the association type qualified name + * @return Return the policies + */ + public Collection

    getList(Set classQNames, QName assocTypeQName) + { + return getList(null, classQNames, assocTypeQName); + } + + /** + * Gets the Policy instances for all the given Classes and Associations + * + * @param nodeRef the node reference + * @param classQNames the class qualified names + * @param assocTypeQName the association type qualified name + * @return Return the policies + */ + public Collection

    getList(NodeRef nodeRef, Set classQNames, QName assocTypeQName) + { + checkAssocType(assocTypeQName); + Collection

    policies = new HashSet

    (); + for (QName classQName : classQNames) + { + P policy = factory.create(new ClassFeatureBehaviourBinding(dictionary, nodeRef, classQName, assocTypeQName)); + if (policy instanceof PolicyList) + { + policies.addAll(((PolicyList

    )policy).getPolicies()); + } + else + { + policies.add(policy); + } + } + return policies; + } + +} diff --git a/source/java/org/alfresco/repo/policy/Behaviour.java b/source/java/org/alfresco/repo/policy/Behaviour.java new file mode 100644 index 0000000000..2faa7a2dbb --- /dev/null +++ b/source/java/org/alfresco/repo/policy/Behaviour.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * A Behaviour represents an encapsulated piece of logic (system or business) + * that may be bound to a Policy. The logic may be expressed in any + * language (java, script etc). + * + * Once bound to a Policy, the behaviour must be able to provide the interface + * declared by that policy. + * + * @author David Caruana + */ +public interface Behaviour +{ + /** + * Gets the requested policy interface onto the behaviour + * + * @param policy the policy interface class + * @return the policy interface + */ + public T getInterface(Class policy); + + /** + * Disable the behaviour (for this thread only) + */ + public void disable(); + + /** + * Enable the behaviour (for this thread only) + */ + public void enable(); + + /** + * @return is the behaviour enabled (for this thread only) + */ + public boolean isEnabled(); + +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourBinding.java b/source/java/org/alfresco/repo/policy/BehaviourBinding.java new file mode 100644 index 0000000000..9c2d8c831b --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourBinding.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * A Behaviour Binding represents the way in which a Behaviour is bound + * to a Policy i.e. the key. + * + * @author David Caruana + * + */ +/*package*/ interface BehaviourBinding +{ + /** + * Gets a generalised form of the Binding. + * + * For example, if the binding key is hierarchical, return the parent + * key. + * + * @return the generalised form (or null, if there isn't one) + */ + BehaviourBinding generaliseBinding(); +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourChangeObserver.java b/source/java/org/alfresco/repo/policy/BehaviourChangeObserver.java new file mode 100644 index 0000000000..b1bc06b547 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourChangeObserver.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * An Observer interface for listening to changes in behaviour bindings. + * + * @author David Caruana + * + * @param The specific type of Behaviour Binding to listen out for. + */ +/*package*/ interface BehaviourChangeObserver +{ + /** + * A new binding has been made. + * + * @param binding the binding + * @param behaviour the behaviour attached to the binding + */ + public void addition(B binding, Behaviour behaviour); +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourDefinition.java b/source/java/org/alfresco/repo/policy/BehaviourDefinition.java new file mode 100644 index 0000000000..245b4815e4 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourDefinition.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import org.alfresco.service.namespace.QName; + + +/** + * Description of a bound Behaviour. + * + * @author David Caruana + * + * @param The type of Binding. + */ +public interface BehaviourDefinition +{ + /** + * Gets the Policy bound to + * + * @return the policy name + */ + public QName getPolicy(); + + /** + * Gets the definition of the Policy bound to + * + * @return the policy definition (or null, if the Policy has not been registered yet) + */ + public PolicyDefinition getPolicyDefinition(); + + /** + * Gets the binding used to bind the Behaviour to the Policy + * + * @return the binding + */ + public B getBinding(); + + /** + * Gets the Behaviour + * + * @return the behaviour + */ + public Behaviour getBehaviour(); +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourFilter.java b/source/java/org/alfresco/repo/policy/BehaviourFilter.java new file mode 100644 index 0000000000..9c81af1fd7 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourFilter.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Contract disabling and enabling policy behaviours. + * + * @author David Caruana + */ +public interface BehaviourFilter +{ + /** + * Disable behaviour for all nodes + * + * @param className the type/aspect behaviour to disable + * @return true => already disabled + */ + public boolean disableBehaviour(QName className); + + /** + * Disable behaviour for specific node + * + * @param nodeRef the node to disable for + * @param className the type/aspect behaviour to disable + * @return true => already disabled + */ + public boolean disableBehaviour(NodeRef nodeRef, QName className); + + /** + * Enable behaviour for all nodes + * + * @param className the type/aspect behaviour to enable + */ + public void enableBehaviour(QName className); + + /** + * Enable behaviour for specific node + * + * @param nodeRef the node to enable for + * @param className the type/aspect behaviour to enable + */ + public void enableBehaviour(NodeRef nodeRef, QName className); + + /** + * Enable all behaviours for specific node + * + * @param nodeRef the node to enable for + */ + public void enableBehaviours(NodeRef nodeRef); + + /** + * Enable all behaviours + */ + public void enableAllBehaviours(); + + /** + * Determine if behaviour is enabled across all nodes. + * + * @param className the behaviour to test for + * @return true => behaviour is enabled + */ + public boolean isEnabled(QName className); + + /** + * Determine if behaviour is enabled for specific node. + * + * Note: A node behaviour is enabled only when: + * a) the behaviour is not disabled across all nodes + * b) the behaviour is not disabled specifically for the provided node + * + * @param nodeRef the node to test for + * @param className the behaviour to test for + * @return true => behaviour is enabled + */ + public boolean isEnabled(NodeRef nodeRef, QName className); + + /** + * Determine if any behaviours have been disabled? + * + * @return true => behaviours have been filtered + */ + public boolean isActivated(); +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourFilterImpl.java b/source/java/org/alfresco/repo/policy/BehaviourFilterImpl.java new file mode 100644 index 0000000000..9a00ebee3b --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourFilterImpl.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Implementation of Behaviour Filter. + * + * @author David Caruana + */ +public class BehaviourFilterImpl implements BehaviourFilter +{ + // Thread local storage of filters + ThreadLocal> classFilter = new ThreadLocal>(); + ThreadLocal>> nodeRefFilter = new ThreadLocal>>(); + + // Dictionary Service + private DictionaryService dictionaryService; + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#disableBehaviour(org.alfresco.service.namespace.QName) + */ + public boolean disableBehaviour(QName className) + { + List classNames = classFilter.get(); + if (classNames == null) + { + classNames = new ArrayList(); + classFilter.set(classNames); + } + boolean alreadyDisabled = classNames.contains(className); + if (!alreadyDisabled) + { + classNames.add(className); + } + return alreadyDisabled; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#disableBehaviour(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public boolean disableBehaviour(NodeRef nodeRef, QName className) + { + Map> filters = nodeRefFilter.get(); + if (filters == null) + { + filters = new HashMap>(); + nodeRefFilter.set(filters); + } + List classNames = filters.get(nodeRef); + if (classNames == null) + { + classNames = new ArrayList(); + filters.put(nodeRef, classNames); + } + boolean alreadyDisabled = classNames.contains(className); + if (!alreadyDisabled) + { + classNames.add(className); + } + return alreadyDisabled; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#enableBehaviour(org.alfresco.service.namespace.QName) + */ + public void enableBehaviour(QName className) + { + List classNames = classFilter.get(); + if (classNames != null) + { + classNames.remove(className); + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#enableBehaviour(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void enableBehaviour(NodeRef nodeRef, QName className) + { + Map> filters = nodeRefFilter.get(); + if (filters != null) + { + List classNames = filters.get(nodeRef); + if (classNames != null) + { + classNames.remove(className); + } + if (classNames.size() == 0) + { + filters.remove(nodeRef); + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#enableBehaviours(org.alfresco.service.cmr.repository.NodeRef) + */ + public void enableBehaviours(NodeRef nodeRef) + { + Map> filters = nodeRefFilter.get(); + if (filters != null) + { + filters.remove(nodeRef); + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#enableAllBehaviours() + */ + public void enableAllBehaviours() + { + Map> filters = nodeRefFilter.get(); + if (filters != null) + { + filters.clear(); + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#isEnabled(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public boolean isEnabled(NodeRef nodeRef, QName className) + { + // check global filters + if (!isEnabled(className)) + { + return false; + } + + // check node level filters + Map> nodeFilters = nodeRefFilter.get(); + if (nodeFilters != null) + { + List nodeClassFilters = nodeFilters.get(nodeRef); + if (nodeClassFilters != null) + { + boolean filtered = nodeClassFilters.contains(className); + if (filtered) + { + return false; + } + for (QName filterName : nodeClassFilters) + { + filtered = dictionaryService.isSubClass(className, filterName); + if (filtered) + { + return false; + } + } + } + } + + return true; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#isEnabled(org.alfresco.service.namespace.QName) + */ + public boolean isEnabled(QName className) + { + // check global class filters + List classFilters = classFilter.get(); + if (classFilters != null) + { + boolean filtered = classFilters.contains(className); + if (filtered) + { + return false; + } + for (QName filterName : classFilters) + { + filtered = dictionaryService.isSubClass(className, filterName); + if (filtered) + { + return false; + } + } + } + + return true; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourFilter#isActivated() + */ + public boolean isActivated() + { + List classFilters = classFilter.get(); + Map> nodeFilters = nodeRefFilter.get(); + return (classFilters != null && !classFilters.isEmpty()) || (nodeFilters != null && !nodeFilters.isEmpty()); + } + +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourIndex.java b/source/java/org/alfresco/repo/policy/BehaviourIndex.java new file mode 100644 index 0000000000..438e6123c0 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourIndex.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.Collection; + + +/** + * Index of Bound Behaviours. + * + * @author David Caruana + * + * @param the type of Binding. + */ +/*package*/ interface BehaviourIndex +{ + /** + * Gets all bound behaviours + * + * @return the bound behaviours + */ + public Collection getAll(); + + /** + * Gets all bound behaviours for the specified binding. + * + * Note: The index may use any algorithm for determining which behaviours + * are returned for the binding e.g. based on hierarchical binding + * + * @param binding the binding + * @return the associated behaviours + */ + public Collection find(B binding); + + /** + * Add a Behaviour Change Observer. + * + * @param observer the observer + */ + public void addChangeObserver(BehaviourChangeObserver observer); + + /** + * Gets the behaviour filter + * + * @return the behaviour filter + */ + public BehaviourFilter getFilter(); +} diff --git a/source/java/org/alfresco/repo/policy/BehaviourMap.java b/source/java/org/alfresco/repo/policy/BehaviourMap.java new file mode 100644 index 0000000000..e9e2112b4e --- /dev/null +++ b/source/java/org/alfresco/repo/policy/BehaviourMap.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * Simple Map of Binding to Behaviour with observer support. + * + * @author David Caruana + * + * @param the type of binding. + */ +/*package*/ class BehaviourMap +{ + /** + * The map of bindings to behaviour + */ + private Map> index = new HashMap>(); + + /** + * The list of registered observers + */ + private List> observers = new ArrayList>(); + + + /** + * Binds a Behaviour into the Map + * + * @param behaviourDefinition the behaviour definition to bind + */ + public void put(BehaviourDefinition behaviourDefinition) + { + B binding = behaviourDefinition.getBinding(); + index.put(binding, behaviourDefinition); + for (BehaviourChangeObserver listener : observers) + { + listener.addition(binding, behaviourDefinition.getBehaviour()); + } + } + + + /** + * Gets a Behaviour from the Map + * + * @param binding the binding + * @return the behaviour + */ + public BehaviourDefinition get(B binding) + { + return index.get(binding); + } + + + /** + * Gets all bound Behaviours from the Map + * + * @return all bound behaviours + */ + public Collection> getAll() + { + return index.values(); + } + + + /** + * Gets the count of bound behaviours + * + * @return the count + */ + public int size() + { + return index.size(); + } + + + /** + * Adds a Change Observer + * + * @param observer the change observer + */ + public void addChangeObserver(BehaviourChangeObserver observer) + { + observers.add(observer); + } + +} diff --git a/source/java/org/alfresco/repo/policy/CachedPolicyFactory.java b/source/java/org/alfresco/repo/policy/CachedPolicyFactory.java new file mode 100644 index 0000000000..b9c5d9826b --- /dev/null +++ b/source/java/org/alfresco/repo/policy/CachedPolicyFactory.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Policy Factory with caching support. + * + * @author David Caruana + * + * @param the type of Binding + * @param

    the type of Policy + */ +/*package*/ class CachedPolicyFactory extends PolicyFactory +{ + // Logger + private static final Log logger = LogFactory.getLog(PolicyComponentImpl.class); + + // Behaviour Filter + private BehaviourFilter behaviourFilter = null; + + // Cache Lock + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + /** + * Cache for a single Policy interface (keyed by Binding) + */ + private Map singleCache = new HashMap(); + + /** + * Cache for a collection of Policy interfaces (keyed by Binding) + */ + private Map> listCache = new HashMap>(); + + + /** + * Construct cached policy factory + * + * @param policyClass the policy interface class + * @param index the behaviour index to search on + */ + /*package*/ CachedPolicyFactory(Class

    policyClass, BehaviourIndex index) + { + super(policyClass, index); + behaviourFilter = index.getFilter(); + + // Register this cached policy factory as a change observer of the behaviour index + // to allow for cache to be cleared appropriately. + index.addChangeObserver(new BehaviourChangeObserver() + { + public void addition(B binding, Behaviour behaviour) + { + clearCache("aggregate delegate", singleCache, binding); + clearCache("delegate collection", listCache, binding); + } + }); + } + + + @Override + public P create(B binding) + { + // When behaviour filters are activated bypass the cache + if (behaviourFilter != null && behaviourFilter.isActivated()) + { + return super.create(binding); + } + + lock.readLock().lock(); + + try + { + P policyInterface = singleCache.get(binding); + if (policyInterface == null) + { + // Upgrade read lock to write lock + lock.readLock().unlock(); + lock.writeLock().lock(); + + try + { + // Check again + policyInterface = singleCache.get(binding); + if (policyInterface == null) + { + policyInterface = super.create(binding); + singleCache.put(binding, policyInterface); + + if (logger.isDebugEnabled()) + logger.debug("Cached delegate interface " + policyInterface + " for " + binding + " and policy " + getPolicyClass()); + } + } + finally + { + // Downgrade lock to read + lock.readLock().lock(); + lock.writeLock().unlock(); + } + } + return policyInterface; + } + finally + { + lock.readLock().unlock(); + } + } + + + @Override + public Collection

    createList(B binding) + { + // When behaviour filters are activated bypass the cache + if (behaviourFilter != null && behaviourFilter.isActivated()) + { + return super.createList(binding); + } + + lock.readLock().lock(); + + try + { + Collection

    policyInterfaces = listCache.get(binding); + if (policyInterfaces == null) + { + // Upgrade read lock to write lock + lock.readLock().unlock(); + lock.writeLock().lock(); + + try + { + // Check again + policyInterfaces = listCache.get(binding); + if (policyInterfaces == null) + { + policyInterfaces = super.createList(binding); + listCache.put(binding, policyInterfaces); + + if (logger.isDebugEnabled()) + logger.debug("Cached delegate interface collection " + policyInterfaces + " for " + binding + " and policy " + getPolicyClass()); + } + } + finally + { + // Downgrade lock to read + lock.readLock().lock(); + lock.writeLock().unlock(); + } + } + return policyInterfaces; + } + finally + { + lock.readLock().unlock(); + } + } + + + /** + * Clear entries in the cache based on binding changes. + * + * @param cacheDescription description of cache to clear + * @param cache the cache to clear + * @param binding the binding + */ + private void clearCache(String cacheDescription, Map cache, B binding) + { + if (binding == null) + { + lock.writeLock().lock(); + + try + { + // A specific binding has not been provided, so clear all entries + cache.clear(); + + if (logger.isDebugEnabled() && cache.isEmpty() == false) + logger.debug("Cleared " + cacheDescription + " cache (all class bindings) for policy " + getPolicyClass()); + } + finally + { + lock.writeLock().unlock(); + } + } + else + { + // A specific binding has been provided. Build a list of entries + // that require removal. An entry is removed if the binding in the + // list is equal or derived from the changed binding. + Collection invalidBindings = new ArrayList(); + for (B cachedBinding : cache.keySet()) + { + // Determine if binding is equal or derived from changed binding + BehaviourBinding generalisedBinding = cachedBinding; + while(generalisedBinding != null) + { + if (generalisedBinding.equals(binding)) + { + invalidBindings.add(cachedBinding); + break; + } + generalisedBinding = generalisedBinding.generaliseBinding(); + } + } + + // Remove all invalid bindings + if (invalidBindings.size() > 0) + { + lock.writeLock().lock(); + + try + { + for (B invalidBinding : invalidBindings) + { + cache.remove(invalidBinding); + + if (logger.isDebugEnabled()) + logger.debug("Cleared " + cacheDescription + " cache for " + invalidBinding + " and policy " + getPolicyClass()); + } + } + finally + { + lock.writeLock().unlock(); + } + } + } + } + +} diff --git a/source/java/org/alfresco/repo/policy/ClassBehaviourBinding.java b/source/java/org/alfresco/repo/policy/ClassBehaviourBinding.java new file mode 100644 index 0000000000..8afc83a2ef --- /dev/null +++ b/source/java/org/alfresco/repo/policy/ClassBehaviourBinding.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Behaviour binding to a Class (Type or Aspect) in the Content Model. + * + * @author David Caruana + * + */ +/*package*/ class ClassBehaviourBinding implements BehaviourBinding +{ + // The dictionary service + private DictionaryService dictionary; + + // The class qualified name + private QName classQName; + + // Instance level node reference + private NodeRef nodeRef; + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param nodeRef the instance level node reference + * @param classQName the Class qualified name + */ + /*package*/ ClassBehaviourBinding(DictionaryService dictionary, NodeRef nodeRef, QName classQName) + { + this.dictionary = dictionary; + this.nodeRef = nodeRef; + this.classQName = classQName; + } + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param classQName the Class qualified name + */ + /*package*/ ClassBehaviourBinding(DictionaryService dictionary, QName classQName) + { + this(dictionary, null, classQName); + } + + /*package*/ DictionaryService getDictionary() + { + return dictionary; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourBinding#generaliseBinding() + */ + public BehaviourBinding generaliseBinding() + { + BehaviourBinding generalisedBinding = null; + ClassDefinition classDefinition = dictionary.getClass(classQName); + if (classDefinition == null) + { + throw new PolicyException("Class definition " + classDefinition.getName() + " does not exist."); + } + + QName parentClassName = classDefinition.getParentName(); + if (parentClassName != null) + { + generalisedBinding = new ClassBehaviourBinding(dictionary, parentClassName); + } + return generalisedBinding; + } + + /** + * Gets the instance level node reference + * + * @return the node reference + */ + public NodeRef getNodeRef() + { + return nodeRef; + } + + /** + * Gets the class qualified name + * + * @return the class qualified name + */ + public QName getClassQName() + { + return classQName; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || !(obj instanceof ClassBehaviourBinding)) + { + return false; + } + return classQName.equals(((ClassBehaviourBinding)obj).classQName); + } + + @Override + public int hashCode() + { + return classQName.hashCode(); + } + + @Override + public String toString() + { + return "ClassBinding[class=" + classQName + "]"; + } + +} diff --git a/source/java/org/alfresco/repo/policy/ClassBehaviourIndex.java b/source/java/org/alfresco/repo/policy/ClassBehaviourIndex.java new file mode 100644 index 0000000000..7d511c9862 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/ClassBehaviourIndex.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Class (Type/Aspect) oriented index of bound behaviours + * + * Note: Uses Class hierarchy to derive bindings. + * + * @author David Caruana + * + */ +/*package*/ class ClassBehaviourIndex implements BehaviourIndex +{ + // Lock + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + // Map of class bindings + private BehaviourMap classMap = new BehaviourMap(); + + // Map of service bindings + private BehaviourMap serviceMap = new BehaviourMap(); + + // List of registered observers + private List> observers = new ArrayList>(); + + // Behaviour Filter + private BehaviourFilter filter = null; + + + /** + * Construct. + */ + /*package*/ ClassBehaviourIndex(BehaviourFilter filter) + { + // Observe class binding changes and propagate to our own observers + this.classMap.addChangeObserver(new BehaviourChangeObserver() + { + public void addition(B binding, Behaviour behaviour) + { + for (BehaviourChangeObserver listener : observers) + { + listener.addition(binding, behaviour); + } + } + }); + + // Observe service binding changes and propagate to our own observers + this.serviceMap.addChangeObserver(new BehaviourChangeObserver() + { + public void addition(ServiceBehaviourBinding binding, Behaviour behaviour) + { + for (BehaviourChangeObserver listener : observers) + { + // Note: Don't specify class ref as service-level bindings affect all classes + listener.addition(null, behaviour); + } + } + }); + + // Setup state + this.filter = filter; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourIndex#getAll() + */ + public Collection getAll() + { + lock.readLock().lock(); + + try + { + List all = new ArrayList(classMap.size() + serviceMap.size()); + all.addAll(classMap.getAll()); + all.addAll(serviceMap.getAll()); + return all; + } + finally + { + lock.readLock().unlock(); + } + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourIndex#find() + */ + @SuppressWarnings("unchecked") + public Collection find(B binding) + { + lock.readLock().lock(); + + try + { + List behaviours = new ArrayList(); + + // Determine if behaviour has been disabled + boolean isEnabled = true; + if (filter != null) + { + NodeRef nodeRef = binding.getNodeRef(); + QName className = binding.getClassQName(); + isEnabled = (nodeRef == null) ? filter.isEnabled(className) : filter.isEnabled(nodeRef, className); + } + + if (isEnabled) + { + // Find class behaviour by scanning up the class hierarchy + BehaviourDefinition behaviour = null; + while(behaviour == null && binding != null) + { + behaviour = classMap.get(binding); + if (behaviour == null) + { + binding = (B)binding.generaliseBinding(); + } + } + if (behaviour != null) + { + behaviours.add(behaviour); + } + } + + // Append all service-level behaviours + behaviours.addAll(serviceMap.getAll()); + + return behaviours; + } + finally + { + lock.readLock().unlock(); + } + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourIndex#find() + */ + public void addChangeObserver(BehaviourChangeObserver observer) + { + observers.add(observer); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourIndex#getFilter() + */ + public BehaviourFilter getFilter() + { + return filter; + } + + + /** + * Binds a Class Behaviour into this index + * + * @param behaviour the class bound behaviour + */ + public void putClassBehaviour(BehaviourDefinition behaviour) + { + lock.writeLock().lock(); + try + { + classMap.put(behaviour); + } + finally + { + lock.writeLock().unlock(); + } + } + + + /** + * Binds a Service Behaviour into this index + * + * @param behaviour the service bound behaviour + */ + public void putServiceBehaviour(BehaviourDefinition behaviour) + { + lock.writeLock().lock(); + try + { + serviceMap.put(behaviour); + } + finally + { + lock.writeLock().unlock(); + } + } + +} diff --git a/source/java/org/alfresco/repo/policy/ClassFeatureBehaviourBinding.java b/source/java/org/alfresco/repo/policy/ClassFeatureBehaviourBinding.java new file mode 100644 index 0000000000..7ef7241cc0 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/ClassFeatureBehaviourBinding.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Behaviour binding to a Class (Type or Aspect) in the Content Model. + * + * @author David Caruana + * + */ +/*package*/ class ClassFeatureBehaviourBinding extends ClassBehaviourBinding +{ + // The feature qualified name (property or association) + private QName featureQName; + private QName activeFeatureQName; + + // Wild Card feature match (match all features) + private static final QName ALL_FEATURES = QName.createQName("", "*"); + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param nodeRef the node reference + * @param classQName the Class qualified name + * @param featureQName the Class feature (property or association) qualifed name + */ + /*package*/ ClassFeatureBehaviourBinding(DictionaryService dictionary, NodeRef nodeRef, QName classQName, QName featureQName) + { + this(dictionary, nodeRef, classQName, featureQName, featureQName); + } + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param classQName the Class qualified name + * @param featureQName the Class feature (property or association) qualifed name + */ + /*package*/ ClassFeatureBehaviourBinding(DictionaryService dictionary, QName classQName, QName featureQName) + { + this(dictionary, null, classQName, featureQName, featureQName); + } + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param nodeRef the node reference + * @param classQName the Class qualified name + */ + /*package*/ ClassFeatureBehaviourBinding(DictionaryService dictionary, NodeRef nodeRef, QName classQName) + { + this(dictionary, nodeRef, classQName, ALL_FEATURES); + } + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param classQName the Class qualified name + */ + /*package*/ ClassFeatureBehaviourBinding(DictionaryService dictionary, QName classQName) + { + this(dictionary, null, classQName, ALL_FEATURES); + } + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param nodeRef the node reference + * @param classQName the Class qualified name + * @param featureQName the Class feature (property or association) qualifed name + * @param activeFeatureQName the currently active feature QName + */ + private ClassFeatureBehaviourBinding(DictionaryService dictionary, NodeRef nodeRef, QName classQName, QName featureQName, QName activeFeatureQName) + { + super(dictionary, nodeRef, classQName); + this.featureQName = featureQName; + this.activeFeatureQName = activeFeatureQName; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourBinding#generaliseBinding() + */ + public BehaviourBinding generaliseBinding() + { + BehaviourBinding generalisedBinding = null; + ClassDefinition classDefinition = getDictionary().getClass(getClassQName()); + + if (activeFeatureQName.equals(ALL_FEATURES)) + { + QName parentClassName = classDefinition.getParentName(); + if (parentClassName != null) + { + generalisedBinding = new ClassFeatureBehaviourBinding(getDictionary(), getNodeRef(), parentClassName, featureQName, featureQName); + } + } + else + { + generalisedBinding = new ClassFeatureBehaviourBinding(getDictionary(), getNodeRef(), getClassQName(), featureQName, ALL_FEATURES); + } + + return generalisedBinding; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || !(obj instanceof ClassFeatureBehaviourBinding)) + { + return false; + } + return getClassQName().equals(((ClassFeatureBehaviourBinding)obj).getClassQName()) && + activeFeatureQName.equals(((ClassFeatureBehaviourBinding)obj).activeFeatureQName); + } + + @Override + public int hashCode() + { + return 37 * getClassQName().hashCode() + activeFeatureQName.hashCode(); + } + + @Override + public String toString() + { + return "ClassFeatureBinding[class=" + getClassQName() + ";feature=" + activeFeatureQName + "]"; + } + +} diff --git a/source/java/org/alfresco/repo/policy/ClassPolicy.java b/source/java/org/alfresco/repo/policy/ClassPolicy.java new file mode 100644 index 0000000000..73b43f86d4 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/ClassPolicy.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +/** + * Marker interface for representing an Class-level Policy. + * + * @author David Caruana + * + */ +public interface ClassPolicy extends Policy +{ +} diff --git a/source/java/org/alfresco/repo/policy/ClassPolicyDelegate.java b/source/java/org/alfresco/repo/policy/ClassPolicyDelegate.java new file mode 100644 index 0000000000..99af7126fd --- /dev/null +++ b/source/java/org/alfresco/repo/policy/ClassPolicyDelegate.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Delegate for a Class-level Policy. Provides access to Policy Interface + * implementations which invoke the appropriate bound behaviours. + * + * @author David Caruana + * + * @param

    the policy interface + */ +public class ClassPolicyDelegate

    +{ + private DictionaryService dictionary; + private CachedPolicyFactory factory; + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param policyClass the policy interface class + * @param index the behaviour index to query against + */ + @SuppressWarnings("unchecked") + /*package*/ ClassPolicyDelegate(DictionaryService dictionary, Class

    policyClass, BehaviourIndex index) + { + // Get list of all pre-registered behaviours for the policy and + // ensure they are valid. + Collection definitions = index.getAll(); + for (BehaviourDefinition definition : definitions) + { + definition.getBehaviour().getInterface(policyClass); + } + + // Rely on cached implementation of policy factory + // Note: Could also use PolicyFactory (without caching) + this.factory = new CachedPolicyFactory(policyClass, index); + this.dictionary = dictionary; + } + + + /** + * Gets the Policy implementation for the specified Class + * + * When multiple behaviours are bound to the policy for the class, an + * aggregate policy implementation is returned which invokes each policy + * in turn. + * + * @param classQName the class qualified name + * @return the policy + */ + public P get(QName classQName) + { + return get(null, classQName); + } + + /** + * Gets the Policy implementation for the specified Class + * + * @param nodeRef the node reference + * @param classQName the class name + * @return the policy + */ + public P get(NodeRef nodeRef, QName classQName) + { + ClassDefinition classDefinition = dictionary.getClass(classQName); + if (classDefinition == null) + { + throw new IllegalArgumentException("Class " + classQName + " has not been defined in the data dictionary"); + } + return factory.create(new ClassBehaviourBinding(dictionary, nodeRef, classQName)); + } + + /** + * Gets the collection of Policy implementations for the specified Class + * + * @param classQName the class qualified name + * @return the collection of policies + */ + public Collection

    getList(QName classQName) + { + return getList(null, classQName); + } + + /** + * Gets the collection of Policy implementations for the specified Class + * + * @param nodeRef the node reference + * @param classQName the class qualified name + * @return the collection of policies + */ + public Collection

    getList(NodeRef nodeRef, QName classQName) + { + ClassDefinition classDefinition = dictionary.getClass(classQName); + if (classDefinition == null) + { + throw new IllegalArgumentException("Class " + classQName + " has not been defined in the data dictionary"); + } + return factory.createList(new ClassBehaviourBinding(dictionary, nodeRef, classQName)); + } + + /** + * Gets the policy implementation for the given classes. The single Policy + * will be a wrapper of multiple appropriate policies. + * + * @param classQNames the class qualified names + * @return Returns the policy + */ + public P get(Set classQNames) + { + return get(null, classQNames); + } + + /** + * Gets the policy implementation for the given classes. The single Policy + * will be a wrapper of multiple appropriate policies. + * + * @param nodeRef the node reference + * @param classQNames the class qualified names + * @return Returns the policy + */ + public P get(NodeRef nodeRef, Set classQNames) + { + return factory.toPolicy(getList(nodeRef, classQNames)); + } + + /** + * Gets the collection of Policy implementations for the given classes + * + * @param classQNames the class qualified names + * @return Returns the collection of policies + */ + public Collection

    getList(Set classQNames) + { + return getList(null, classQNames); + } + + /** + * Gets the collection of Policy implementations for the given classes + * + * @param classQNames the class qualified names + * @return Returns the collection of policies + */ + public Collection

    getList(NodeRef nodeRef, Set classQNames) + { + Collection

    policies = new HashSet

    (); + for (QName classQName : classQNames) + { + P policy = factory.create(new ClassBehaviourBinding(dictionary, nodeRef, classQName)); + if (policy instanceof PolicyList) + { + policies.addAll(((PolicyList

    )policy).getPolicies()); + } + else + { + policies.add(policy); + } + } + return policies; + } +} diff --git a/source/java/org/alfresco/repo/policy/JavaBehaviour.java b/source/java/org/alfresco/repo/policy/JavaBehaviour.java new file mode 100644 index 0000000000..3c207c399f --- /dev/null +++ b/source/java/org/alfresco/repo/policy/JavaBehaviour.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import org.alfresco.util.ParameterCheck; + + +/** + * Java based Behaviour. + * + * A behavior acts like a delegate (a method pointer). The pointer is + * represented by an instance object and method name. + * + * @author David Caruana + * + */ +public class JavaBehaviour implements Behaviour +{ + // The object instance holding the method + private Object instance; + + // The method name + private String method; + + // Cache of interface proxies (by interface class) + private Map proxies = new HashMap(); + + // Enable / Disable invocation of behaviour + private StackThreadLocal disabled = new StackThreadLocal(); + + + /** + * Construct. + * + * @param instance the object instance holding the method + * @param method the method name + */ + public JavaBehaviour(Object instance, String method) + { + ParameterCheck.mandatory("Instance", instance); + ParameterCheck.mandatory("Method", method); + this.instance = instance; + this.method = method; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.Behaviour#getInterface(java.lang.Class) + */ + @SuppressWarnings("unchecked") + public synchronized T getInterface(Class policy) + { + ParameterCheck.mandatory("Policy class", policy); + Object proxy = proxies.get(policy); + if (proxy == null) + { + InvocationHandler handler = getInvocationHandler(instance, method, policy); + proxy = Proxy.newProxyInstance(policy.getClassLoader(), new Class[]{policy}, handler); + proxies.put(policy, proxy); + } + return (T)proxy; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.Behaviour#disable() + */ + public void disable() + { + Stack stack = disabled.get(); + stack.push(hashCode()); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.Behaviour#enable() + */ + public void enable() + { + Stack stack = disabled.get(); + if (stack.peek().equals(hashCode()) == false) + { + throw new PolicyException("Cannot enable " + this.toString() + " at this time - mismatched with disable calls"); + } + stack.pop(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.Behaviour#isEnabled() + */ + public boolean isEnabled() + { + Stack stack = disabled.get(); + return stack.search(hashCode()) == -1; + } + + @Override + public String toString() + { + return "Java method[class=" + instance.getClass().getName() + ", method=" + method + "]"; + } + + /** + * Gets the Invocation Handler. + * + * @param the policy interface class + * @param instance the object instance + * @param method the method name + * @param policyIF the policy interface class + * @return the invocation handler + */ + private InvocationHandler getInvocationHandler(Object instance, String method, Class policyIF) + { + Method[] policyIFMethods = policyIF.getMethods(); + if (policyIFMethods.length != 1) + { + throw new PolicyException("Policy interface " + policyIF.getCanonicalName() + " must have only one method"); + } + + try + { + Class instanceClass = instance.getClass(); + Method delegateMethod = instanceClass.getMethod(method, (Class[])policyIFMethods[0].getParameterTypes()); + return new JavaMethodInvocationHandler(this, delegateMethod); + } + catch (NoSuchMethodException e) + { + throw new PolicyException("Method " + method + " not found or accessible on " + instance.getClass(), e); + } + } + + + /** + * Stack specific Thread Local + * + * @author David Caruana + */ + private class StackThreadLocal extends ThreadLocal> + { + @Override + protected Stack initialValue() + { + return new Stack(); + } + } + + + /** + * Java Method Invocation Handler + * + * @author David Caruana + */ + private static class JavaMethodInvocationHandler implements InvocationHandler + { + private JavaBehaviour behaviour; + private Method delegateMethod; + + /** + * Constuct. + * + * @param instance the object instance holding the method + * @param delegateMethod the method to invoke + */ + private JavaMethodInvocationHandler(JavaBehaviour behaviour, Method delegateMethod) + { + this.behaviour = behaviour; + this.delegateMethod = delegateMethod; + } + + /* (non-Javadoc) + * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) + */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + // Handle Object level methods + if (method.getName().equals("toString")) + { + return toString(); + } + else if (method.getName().equals("hashCode")) + { + return hashCode(); + } + else if (method.getName().equals("equals")) + { + if (Proxy.isProxyClass(args[0].getClass())) + { + return equals(Proxy.getInvocationHandler(args[0])); + } + return false; + } + + // Delegate to designated method pointer + if (behaviour.isEnabled()) + { + try + { + behaviour.disable(); + return delegateMethod.invoke(behaviour.instance, args); + } + catch (InvocationTargetException e) + { + throw e.getCause(); + } + finally + { + behaviour.enable(); + } + } + return null; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + { + return true; + } + else if (obj == null || !(obj instanceof JavaMethodInvocationHandler)) + { + return false; + } + JavaMethodInvocationHandler other = (JavaMethodInvocationHandler)obj; + return behaviour.instance.equals(other.behaviour.instance) && delegateMethod.equals(other.delegateMethod); + } + + @Override + public int hashCode() + { + return 37 * behaviour.instance.hashCode() + delegateMethod.hashCode(); + } + + @Override + public String toString() + { + return "JavaBehaviour[instance=" + behaviour.instance.hashCode() + ", method=" + delegateMethod.toString() + "]"; + } + } + +} diff --git a/source/java/org/alfresco/repo/policy/Policy.java b/source/java/org/alfresco/repo/policy/Policy.java new file mode 100644 index 0000000000..828ac234fc --- /dev/null +++ b/source/java/org/alfresco/repo/policy/Policy.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import org.alfresco.service.namespace.NamespaceService; + +/** + * Marker interface for representing a Policy. + * + * @author David Caruana + */ +public interface Policy +{ + /** + * mandatory static field on a Policy that can be overridden in + * derived policies + */ + static String NAMESPACE = NamespaceService.ALFRESCO_URI; +} diff --git a/source/java/org/alfresco/repo/policy/PolicyComponent.java b/source/java/org/alfresco/repo/policy/PolicyComponent.java new file mode 100644 index 0000000000..9071da1fc6 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyComponent.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.Collection; + +import org.alfresco.service.namespace.QName; + + +/** + * Policy Component for managing Policies and Behaviours. + * + * This component provides the ability to: + * + * a) Register policies + * b) Bind behaviours to policies + * c) Invoke policy behaviours + * + * A behaviour may be bound to a Policy before the Policy is registered. In + * this case, the behaviour is not validated (i.e. checked to determine if it + * supports the policy interface) until the Policy is registered. Otherwise, + * the behaviour is validated at bind-time. + * + * @author David Caruana + * + */ +public interface PolicyComponent +{ + /** + * Register a Class-level Policy + * + * @param

    the policy interface + * @param policy the policy interface class + * @return A delegate for the class-level policy (typed by the policy interface) + */ + public

    ClassPolicyDelegate

    registerClassPolicy(Class

    policy); + + /** + * Register a Property-level Policy + * + * @param

    the policy interface + * @param policy the policy interface class + * @return A delegate for the property-level policy (typed by the policy interface) + */ + public

    PropertyPolicyDelegate

    registerPropertyPolicy(Class

    policy); + + /** + * Register a Association-level Policy + * + * @param

    the policy interface + * @param policy the policy interface class + * @return A delegate for the association-level policy (typed by the policy interface) + */ + public

    AssociationPolicyDelegate

    registerAssociationPolicy(Class

    policy); + + /** + * Gets all registered Policies + * + * @return the collection of registered policy definitions + */ + public Collection getRegisteredPolicies(); + + /** + * Gets the specified registered Policy + * + * @param policyType the policy type + * @param policy the policy name + * @return the policy definition (or null, if it has not been registered) + */ + public PolicyDefinition getRegisteredPolicy(PolicyType policyType, QName policy); + + /** + * Determine if the specified policy has been registered + * + * @param policyType the policy type + * @param policy the policy name + * @return true => registered, false => not yet + */ + public boolean isRegisteredPolicy(PolicyType policyType, QName policy); + + /** + * Bind a Class specific behaviour to a Class-level Policy + * + * @param policy the policy name + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindClassBehaviour(QName policy, QName classRef, Behaviour behaviour); + + /** + * Bind a Service behaviour to a Class-level Policy + * + * @param policy the policy name + * @param service the service (any object, in fact) + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindClassBehaviour(QName policy, Object service, Behaviour behaviour); + + /** + * Bind a Property specific behaviour to a Property-level Policy + * + * @param policy the policy name + * @param className the class to bind against + * @param propertyName the property to bind against + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindPropertyBehaviour(QName policy, QName className, QName propertyName, Behaviour behaviour); + + /** + * Bind a Property specific behaviour to a Property-level Policy (for all properties of a Class) + * + * @param policy the policy name + * @param className the class to bind against + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindPropertyBehaviour(QName policy, QName className, Behaviour behaviour); + + /** + * Bind a Service specific behaviour to a Property-level Policy + * + * @param policy the policy name + * @param service the binding service + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindPropertyBehaviour(QName policy, Object service, Behaviour behaviour); + + /** + * Bind an Association specific behaviour to an Association-level Policy + * + * @param policy the policy name + * @param className the class to bind against + * @param assocRef the association to bind against + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindAssociationBehaviour(QName policy, QName className, QName assocName, Behaviour behaviour); + + /** + * Bind an Association specific behaviour to an Association-level Policy (for all associations of a Class) + * + * @param policy the policy name + * @param className the class to bind against + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindAssociationBehaviour(QName policy, QName className, Behaviour behaviour); + + /** + * Bind a Service specific behaviour to an Association-level Policy + * + * @param policy the policy name + * @param service the binding service + * @param behaviour the behaviour + * @return the registered behaviour definition + */ + public BehaviourDefinition bindAssociationBehaviour(QName policy, Object service, Behaviour behaviour); + +} + + diff --git a/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java b/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java new file mode 100644 index 0000000000..1ec87220c2 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyComponentImpl.java @@ -0,0 +1,666 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Policy Component Implementation. + * + * @author David Caruana + * + */ +public class PolicyComponentImpl implements PolicyComponent +{ + // Logger + private static final Log logger = LogFactory.getLog(PolicyComponentImpl.class); + + // Policy interface annotations + private static String ANNOTATION_NAMESPACE = "NAMESPACE"; + + // Dictionary Service + private DictionaryService dictionary; + + // Behaviour Filter + private BehaviourFilter behaviourFilter; + + // Map of registered Policies + private Map registeredPolicies;; + + // Map of Class Behaviours (by policy name) + private Map> classBehaviours = new HashMap>(); + + // Map of Property Behaviours (by policy name) + private Map> propertyBehaviours = new HashMap>(); + + // Map of Association Behaviours (by policy name) + private Map> associationBehaviours = new HashMap>(); + + // Wild Card Feature + private static final QName FEATURE_WILDCARD = QName.createQName(NamespaceService.DEFAULT_URI, "*"); + + + /** + * Construct + * + * @param dictionary dictionary service + * @param behaviourFilter behaviour filter + */ + public PolicyComponentImpl(DictionaryService dictionary) + { + this.dictionary = dictionary; + this.registeredPolicies = new HashMap(); + } + + + /** + * Sets the behaviour filter + * + * @param filter + */ + public void setBehaviourFilter(BehaviourFilter filter) + { + this.behaviourFilter = filter; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#registerClassPolicy() + */ + @SuppressWarnings("unchecked") + public

    ClassPolicyDelegate

    registerClassPolicy(Class

    policy) + { + ParameterCheck.mandatory("Policy interface class", policy); + PolicyDefinition definition = createPolicyDefinition(policy); + registeredPolicies.put(new PolicyKey(definition.getType(), definition.getName()), definition); + ClassPolicyDelegate

    delegate = new ClassPolicyDelegate

    (dictionary, policy, getClassBehaviourIndex(definition.getName())); + + if (logger.isInfoEnabled()) + logger.info("Registered class policy " + definition.getName() + " (" + definition.getPolicyInterface() + ")"); + + return delegate; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#registerPropertyPolicy(java.lang.Class) + */ + @SuppressWarnings("unchecked") + public

    PropertyPolicyDelegate

    registerPropertyPolicy(Class

    policy) + { + ParameterCheck.mandatory("Policy interface class", policy); + PolicyDefinition definition = createPolicyDefinition(policy); + registeredPolicies.put(new PolicyKey(definition.getType(), definition.getName()), definition); + PropertyPolicyDelegate

    delegate = new PropertyPolicyDelegate

    (dictionary, policy, getPropertyBehaviourIndex(definition.getName())); + + if (logger.isInfoEnabled()) + logger.info("Registered property policy " + definition.getName() + " (" + definition.getPolicyInterface() + ")"); + + return delegate; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#registerAssociationPolicy(java.lang.Class) + */ + @SuppressWarnings("unchecked") + public

    AssociationPolicyDelegate

    registerAssociationPolicy(Class

    policy) + { + ParameterCheck.mandatory("Policy interface class", policy); + PolicyDefinition definition = createPolicyDefinition(policy); + registeredPolicies.put(new PolicyKey(definition.getType(), definition.getName()), definition); + AssociationPolicyDelegate

    delegate = new AssociationPolicyDelegate

    (dictionary, policy, getAssociationBehaviourIndex(definition.getName())); + + if (logger.isInfoEnabled()) + logger.info("Registered association policy " + definition.getName() + " (" + definition.getPolicyInterface() + ")"); + + return delegate; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#getRegisteredPolicies() + */ + public Collection getRegisteredPolicies() + { + return Collections.unmodifiableCollection(registeredPolicies.values()); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#getRegisteredPolicy(org.alfresco.repo.policy.PolicyType, org.alfresco.repo.ref.QName) + */ + public PolicyDefinition getRegisteredPolicy(PolicyType policyType, QName policy) + { + return registeredPolicies.get(new PolicyKey(policyType, policy)); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#isRegisteredPolicy(org.alfresco.repo.policy.PolicyType, org.alfresco.repo.ref.QName) + */ + public boolean isRegisteredPolicy(PolicyType policyType, QName policy) + { + return registeredPolicies.containsKey(new PolicyKey(policyType, policy)); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindClassBehaviour(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindClassBehaviour(QName policy, QName classRef, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Class Reference", classRef); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Validate Binding + ClassDefinition classDefinition = dictionary.getClass(classRef); + if (classDefinition == null) + { + throw new IllegalArgumentException("Class " + classRef + " has not been defined in the data dictionary"); + } + + // Create behaviour definition and bind to policy + ClassBehaviourBinding binding = new ClassBehaviourBinding(dictionary, classRef); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Class, policy, binding, behaviour); + getClassBehaviourIndex(policy).putClassBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to policy " + policy + " for class " + classRef); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindClassBehaviour(org.alfresco.repo.ref.QName, java.lang.Object, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindClassBehaviour(QName policy, Object service, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Service", service); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Create behaviour definition and bind to policy + ServiceBehaviourBinding binding = new ServiceBehaviourBinding(service); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Class, policy, binding, behaviour); + getClassBehaviourIndex(policy).putServiceBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to policy " + policy + " for service " + service); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindPropertyBehaviour(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindPropertyBehaviour(QName policy, QName className, QName propertyName, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Class Reference", className); + ParameterCheck.mandatory("Property Reference", propertyName); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Validate Binding + PropertyDefinition propertyDefinition = dictionary.getProperty(className, propertyName); + if (propertyDefinition == null) + { + throw new IllegalArgumentException("Property " + propertyName + " of class " + className + " has not been defined in the data dictionary"); + } + + // Create behaviour definition and bind to policy + ClassFeatureBehaviourBinding binding = new ClassFeatureBehaviourBinding(dictionary, className, propertyName); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Property, policy, binding, behaviour); + getPropertyBehaviourIndex(policy).putClassBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to policy " + policy + " for property " + propertyName + " of class " + className); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindPropertyBehaviour(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindPropertyBehaviour(QName policy, QName className, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Class Reference", className); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Validate Binding + ClassDefinition classDefinition = dictionary.getClass(className); + if (classDefinition == null) + { + throw new IllegalArgumentException("Class " + className + " has not been defined in the data dictionary"); + } + + // Create behaviour definition and bind to policy + ClassFeatureBehaviourBinding binding = new ClassFeatureBehaviourBinding(dictionary, className, FEATURE_WILDCARD); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Property, policy, binding, behaviour); + getPropertyBehaviourIndex(policy).putClassBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to policy " + policy + " for property " + FEATURE_WILDCARD + " of class " + className); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindPropertyBehaviour(org.alfresco.repo.ref.QName, java.lang.Object, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindPropertyBehaviour(QName policy, Object service, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Service", service); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Create behaviour definition and bind to policy + ServiceBehaviourBinding binding = new ServiceBehaviourBinding(service); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Property, policy, binding, behaviour); + getPropertyBehaviourIndex(policy).putServiceBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to property policy " + policy + " for service " + service); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindAssociationBehaviour(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindAssociationBehaviour(QName policy, QName className, QName assocName, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Class Reference", className); + ParameterCheck.mandatory("Association Reference", assocName); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Validate Binding + AssociationDefinition assocDefinition = dictionary.getAssociation(assocName); + if (assocDefinition == null) + { + throw new IllegalArgumentException("Association " + assocName + " of class " + className + " has not been defined in the data dictionary"); + } + + // Create behaviour definition and bind to policy + ClassFeatureBehaviourBinding binding = new ClassFeatureBehaviourBinding(dictionary, className, assocName); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Association, policy, binding, behaviour); + getAssociationBehaviourIndex(policy).putClassBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to policy " + policy + " for association " + assocName + " of class " + className); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindAssociationBehaviour(org.alfresco.repo.ref.QName, org.alfresco.repo.ref.QName, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindAssociationBehaviour(QName policy, QName className, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Class Reference", className); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Validate Binding + ClassDefinition classDefinition = dictionary.getClass(className); + if (classDefinition == null) + { + throw new IllegalArgumentException("Class " + className + " has not been defined in the data dictionary"); + } + + // Create behaviour definition and bind to policy + ClassFeatureBehaviourBinding binding = new ClassFeatureBehaviourBinding(dictionary, className, FEATURE_WILDCARD); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Association, policy, binding, behaviour); + getAssociationBehaviourIndex(policy).putClassBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to policy " + policy + " for association " + FEATURE_WILDCARD + " of class " + className); + + return definition; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyComponent#bindAssociationBehaviour(org.alfresco.repo.ref.QName, java.lang.Object, org.alfresco.repo.policy.Behaviour) + */ + public BehaviourDefinition bindAssociationBehaviour(QName policy, Object service, Behaviour behaviour) + { + // Validate arguments + ParameterCheck.mandatory("Policy", policy); + ParameterCheck.mandatory("Service", service); + ParameterCheck.mandatory("Behaviour", behaviour); + + // Create behaviour definition and bind to policy + ServiceBehaviourBinding binding = new ServiceBehaviourBinding(service); + BehaviourDefinition definition = createBehaviourDefinition(PolicyType.Association, policy, binding, behaviour); + getAssociationBehaviourIndex(policy).putServiceBehaviour(definition); + + if (logger.isInfoEnabled()) + logger.info("Bound " + behaviour + " to association policy " + policy + " for service " + service); + + return definition; + } + + + /** + * Gets the Class behaviour index for the specified Policy + * + * @param policy the policy + * @return the class behaviour index + */ + private synchronized ClassBehaviourIndex getClassBehaviourIndex(QName policy) + { + ClassBehaviourIndex index = classBehaviours.get(policy); + if (index == null) + { + index = new ClassBehaviourIndex(behaviourFilter); + classBehaviours.put(policy, index); + } + return index; + } + + + /** + * Gets the Property behaviour index for the specified Policy + * + * @param policy the policy + * @return the property behaviour index + */ + private synchronized ClassBehaviourIndex getPropertyBehaviourIndex(QName policy) + { + ClassBehaviourIndex index = propertyBehaviours.get(policy); + if (index == null) + { + index = new ClassBehaviourIndex(behaviourFilter); + propertyBehaviours.put(policy, index); + } + return index; + } + + + /** + * Gets the Association behaviour index for the specified Policy + * + * @param policy the policy + * @return the association behaviour index + */ + private synchronized ClassBehaviourIndex getAssociationBehaviourIndex(QName policy) + { + ClassBehaviourIndex index = associationBehaviours.get(policy); + if (index == null) + { + index = new ClassBehaviourIndex(behaviourFilter); + associationBehaviours.put(policy, index); + } + return index; + } + + + /** + * Create a Behaviour Definition + * + * @param the type of binding + * @param type policy type + * @param policy policy name + * @param binding the binding + * @param behaviour the behaviour + * @return the behaviour definition + */ + @SuppressWarnings("unchecked") + private BehaviourDefinition createBehaviourDefinition(PolicyType type, QName policy, B binding, Behaviour behaviour) + { + // Determine if policy has already been registered + PolicyDefinition policyDefinition = getRegisteredPolicy(type, policy); + if (policyDefinition != null) + { + // Policy has already been registered, force validation of behaviour now + behaviour.getInterface(policyDefinition.getPolicyInterface()); + } + else + { + if (logger.isInfoEnabled()) + logger.info("Behaviour " + behaviour + " is binding (" + binding + ") to policy " + policy + " before the policy is registered"); + } + + // Construct the definition + return new BehaviourDefinitionImpl(type, policy, binding, behaviour); + } + + + /** + * Create a Policy Definition + * + * @param policyIF the policy interface + * @return the policy definition + */ + private PolicyDefinition createPolicyDefinition(Class policyIF) + { + // Extract Policy Namespace + String namespaceURI = NamespaceService.DEFAULT_URI; + try + { + Field metadata = policyIF.getField(ANNOTATION_NAMESPACE); + if (!String.class.isAssignableFrom(metadata.getType())) + { + throw new PolicyException("NAMESPACE metadata incorrectly specified in policy " + policyIF.getCanonicalName()); + } + namespaceURI = (String)metadata.get(null); + } + catch(NoSuchFieldException e) + { + // Assume default namespace + } + catch(IllegalAccessException e) + { + // Shouldn't get here (interface definitions must be accessible) + } + + // Extract Policy Name + Method[] methods = policyIF.getMethods(); + if (methods.length != 1) + { + throw new PolicyException("Policy " + policyIF.getCanonicalName() + " must declare only one method"); + } + String name = methods[0].getName(); + + // Create Policy Definition + return new PolicyDefinitionImpl(QName.createQName(namespaceURI, name), policyIF); + } + + + /** + * Policy Key (composite of policy type and name) + * + * @author David Caruana + * + */ + private static class PolicyKey + { + private PolicyType type; + private QName policy; + + private PolicyKey(PolicyType type, QName policy) + { + this.type = type; + this.policy = policy; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + { + return true; + } + else if (obj == null || !(obj instanceof PolicyKey)) + { + return false; + } + PolicyKey other = (PolicyKey)obj; + return type.equals(other.type) && policy.equals(other.policy); + } + + @Override + public int hashCode() + { + return 37 * type.hashCode() + policy.hashCode(); + } + } + + + /** + * Policy Definition implementation. + * + * @author David Caruana + * + */ + /*package*/ class PolicyDefinitionImpl implements PolicyDefinition + { + private QName policy; + private Class policyIF; + + /*package*/ PolicyDefinitionImpl(QName policy, Class policyIF) + { + this.policy = policy; + this.policyIF = policyIF; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyDefinition#getName() + */ + public QName getName() + { + return policy; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyDefinition#getPolicyInterface() + */ + public Class getPolicyInterface() + { + return policyIF; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyDefinition#getType() + */ + public PolicyType getType() + { + if (ClassPolicy.class.isAssignableFrom(policyIF)) + { + return PolicyType.Class; + } + else if (PropertyPolicy.class.isAssignableFrom(policyIF)) + { + return PolicyType.Property; + } + else + { + return PolicyType.Association; + } + } + } + + + /** + * Behaviour Definition implementation. + * + * @author David Caruana + * + * @param the type of binding + */ + /*package*/ class BehaviourDefinitionImpl implements BehaviourDefinition + { + private PolicyType type; + private QName policy; + private B binding; + private Behaviour behaviour; + + /*package*/ BehaviourDefinitionImpl(PolicyType type, QName policy, B binding, Behaviour behaviour) + { + this.type = type; + this.policy = policy; + this.binding = binding; + this.behaviour = behaviour; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourDefinition#getPolicy() + */ + public QName getPolicy() + { + return policy; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourDefinition#getPolicyDefinition() + */ + public PolicyDefinition getPolicyDefinition() + { + return getRegisteredPolicy(type, policy); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourDefinition#getBinding() + */ + public B getBinding() + { + return binding; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourDefinition#getBehaviour() + */ + public Behaviour getBehaviour() + { + return behaviour; + } + } + +} diff --git a/source/java/org/alfresco/repo/policy/PolicyComponentTest.java b/source/java/org/alfresco/repo/policy/PolicyComponentTest.java new file mode 100644 index 0000000000..ee05bc73ab --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyComponentTest.java @@ -0,0 +1,588 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.repo.dictionary.DictionaryBootstrap; +import org.alfresco.repo.dictionary.DictionaryComponent; +import org.alfresco.repo.dictionary.DictionaryDAOImpl; +import org.alfresco.repo.dictionary.NamespaceDAO; +import org.alfresco.repo.dictionary.NamespaceDAOImpl; +import org.alfresco.service.namespace.QName; + + +public class PolicyComponentTest extends TestCase +{ + private static final String TEST_MODEL = "org/alfresco/repo/policy/policycomponenttest_model.xml"; + private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/policycomponenttest/1.0"; + private static QName BASE_TYPE = QName.createQName(TEST_NAMESPACE, "base"); + private static QName BASE_PROP_A = QName.createQName(TEST_NAMESPACE, "base_a"); + private static QName BASE_ASSOC_A = QName.createQName(TEST_NAMESPACE, "base_assoc_a"); + private static QName FILE_TYPE = QName.createQName(TEST_NAMESPACE, "file"); + private static QName FILE_PROP_B = QName.createQName(TEST_NAMESPACE, "file_b"); + private static QName FOLDER_TYPE = QName.createQName(TEST_NAMESPACE, "folder"); + private static QName FOLDER_PROP_D = QName.createQName(TEST_NAMESPACE, "folder_d"); + private static QName TEST_ASPECT = QName.createQName(TEST_NAMESPACE, "aspect"); + private static QName ASPECT_PROP_A = QName.createQName(TEST_NAMESPACE, "aspect_a"); + private static QName INVALID_TYPE = QName.createQName(TEST_NAMESPACE, "classdoesnotexist"); + + private PolicyComponent policyComponent = null; + + + @Override + protected void setUp() throws Exception + { + // Instantiate Dictionary Service + NamespaceDAO namespaceDAO = new NamespaceDAOImpl(); + DictionaryDAOImpl dictionaryDAO = new DictionaryDAOImpl(namespaceDAO); + + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + List bootstrapModels = new ArrayList(); + bootstrapModels.add("alfresco/model/dictionaryModel.xml"); + bootstrapModels.add("org/alfresco/repo/policy/policycomponenttest_model.xml"); + bootstrapModels.add(TEST_MODEL); + bootstrap.setModels(bootstrapModels); + bootstrap.setDictionaryDAO(dictionaryDAO); + bootstrap.bootstrap(); + + DictionaryComponent dictionary = new DictionaryComponent(); + dictionary.setDictionaryDAO(dictionaryDAO); + + // Instantiate Policy Component + policyComponent = new PolicyComponentImpl(dictionary); + } + + + public void testJavaBehaviour() + { + Behaviour validBehaviour = new JavaBehaviour(this, "validTest"); + TestClassPolicy policy = validBehaviour.getInterface(TestClassPolicy.class); + assertNotNull(policy); + String result = policy.test("argument"); + assertEquals("ValidTest: argument", result); + } + + + @SuppressWarnings("unchecked") + public void testRegisterDefinitions() + { + try + { + @SuppressWarnings("unused") ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(InvalidMetaDataPolicy.class); + fail("Failed to catch hidden metadata"); + } + catch(PolicyException e) + { + } + + try + { + @SuppressWarnings("unused") ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(NoMethodPolicy.class); + fail("Failed to catch no methods defined in policy"); + } + catch(PolicyException e) + { + } + + try + { + @SuppressWarnings("unused") ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(MultiMethodPolicy.class); + fail("Failed to catch multiple methods defined in policy"); + } + catch(PolicyException e) + { + } + + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + boolean isRegistered = policyComponent.isRegisteredPolicy(PolicyType.Class, policyName); + assertFalse(isRegistered); + ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(TestClassPolicy.class); + assertNotNull(delegate); + isRegistered = policyComponent.isRegisteredPolicy(PolicyType.Class, policyName); + assertTrue(isRegistered); + PolicyDefinition definition = policyComponent.getRegisteredPolicy(PolicyType.Class, policyName); + assertNotNull(definition); + assertEquals(policyName, definition.getName()); + assertEquals(PolicyType.Class, definition.getType()); + assertEquals(TestClassPolicy.class, definition.getPolicyInterface()); + } + + + public void testBindBehaviour() + { + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour validBehaviour = new JavaBehaviour(this, "validTest"); + + // Test null policy + try + { + policyComponent.bindClassBehaviour(null, FILE_TYPE, validBehaviour); + fail("Failed to catch null policy whilst binding behaviour"); + } + catch(IllegalArgumentException e) {} + + // Test null Class Reference + try + { + policyComponent.bindClassBehaviour(policyName, null, validBehaviour); + fail("Failed to catch null class reference whilst binding behaviour"); + } + catch(IllegalArgumentException e) {} + + // Test invalid Class Reference + try + { + policyComponent.bindClassBehaviour(policyName, INVALID_TYPE, validBehaviour); + fail("Failed to catch invalid class reference whilst binding behaviour"); + } + catch(IllegalArgumentException e) {} + + // Test null Behaviour + try + { + policyComponent.bindClassBehaviour(policyName, FILE_TYPE, null); + fail("Failed to catch null behaviour whilst binding behaviour"); + } + catch(IllegalArgumentException e) {} + + // Test invalid behaviour (for registered policy) + Behaviour invalidBehaviour = new JavaBehaviour(this, "methoddoesnotexist"); + policyComponent.registerClassPolicy(TestClassPolicy.class); + try + { + policyComponent.bindClassBehaviour(policyName, FILE_TYPE, invalidBehaviour); + fail("Failed to catch invalid behaviour whilst binding behaviour"); + } + catch(PolicyException e) {} + + // Test valid behaviour (for registered policy) + try + { + BehaviourDefinition definition = policyComponent.bindClassBehaviour(policyName, FILE_TYPE, validBehaviour); + assertNotNull(definition); + assertEquals(policyName, definition.getPolicy()); + assertEquals(FILE_TYPE, definition.getBinding().getClassQName()); + } + catch(PolicyException e) + { + fail("Policy exception thrown for valid behaviour"); + } + } + + + public void testClassDelegate() + { + // Register Policy + ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(TestClassPolicy.class); + + // Bind Class Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour fileBehaviour = new JavaBehaviour(this, "fileTest"); + policyComponent.bindClassBehaviour(policyName, FILE_TYPE, fileBehaviour); + + // Test NOOP Policy delegate + Collection basePolicies = delegate.getList(BASE_TYPE); + assertNotNull(basePolicies); + assertTrue(basePolicies.size() == 0); + TestClassPolicy basePolicy = delegate.get(BASE_TYPE); + assertNotNull(basePolicy); + + // Test single Policy delegate + Collection filePolicies = delegate.getList(FILE_TYPE); + assertNotNull(filePolicies); + assertTrue(filePolicies.size() == 1); + TestClassPolicy filePolicy = delegate.get(FILE_TYPE); + assertNotNull(filePolicy); + assertEquals(filePolicies.iterator().next(), filePolicy); + + // Bind Service Behaviour + Behaviour serviceBehaviour = new JavaBehaviour(this, "serviceTest"); + policyComponent.bindClassBehaviour(policyName, this, serviceBehaviour); + + // Test multi Policy delegate + Collection file2Policies = delegate.getList(FILE_TYPE); + assertNotNull(file2Policies); + assertTrue(file2Policies.size() == 2); + TestClassPolicy filePolicy2 = delegate.get(FILE_TYPE); + assertNotNull(filePolicy2); + } + + + public void testClassOverride() + { + // Register Policy + ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(TestClassPolicy.class); + + // Bind Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour baseBehaviour = new JavaBehaviour(this, "baseTest"); + policyComponent.bindClassBehaviour(policyName, BASE_TYPE, baseBehaviour); + Behaviour folderBehaviour = new JavaBehaviour(this, "folderTest"); + policyComponent.bindClassBehaviour(policyName, FOLDER_TYPE, folderBehaviour); + + // Invoke Policies + TestClassPolicy basePolicy = delegate.get(BASE_TYPE); + String baseResult = basePolicy.test("base"); + assertEquals("Base: base", baseResult); + TestClassPolicy filePolicy = delegate.get(FILE_TYPE); + String fileResult = filePolicy.test("file"); + assertEquals("Base: file", fileResult); + TestClassPolicy folderPolicy = delegate.get(FOLDER_TYPE); + String folderResult = folderPolicy.test("folder"); + assertEquals("Folder: folder", folderResult); + } + + + public void testClassCache() + { + // Register Policy + ClassPolicyDelegate delegate = policyComponent.registerClassPolicy(TestClassPolicy.class); + + // Bind Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour baseBehaviour = new JavaBehaviour(this, "baseTest"); + policyComponent.bindClassBehaviour(policyName, BASE_TYPE, baseBehaviour); + Behaviour folderBehaviour = new JavaBehaviour(this, "folderTest"); + policyComponent.bindClassBehaviour(policyName, FOLDER_TYPE, folderBehaviour); + + // Invoke Policies + TestClassPolicy basePolicy = delegate.get(BASE_TYPE); + String baseResult = basePolicy.test("base"); + assertEquals("Base: base", baseResult); + TestClassPolicy filePolicy = delegate.get(FILE_TYPE); + String fileResult = filePolicy.test("file"); + assertEquals("Base: file", fileResult); + TestClassPolicy folderPolicy = delegate.get(FOLDER_TYPE); + String folderResult = folderPolicy.test("folder"); + assertEquals("Folder: folder", folderResult); + + // Retrieve delegates again + TestClassPolicy basePolicy2 = delegate.get(BASE_TYPE); + assertTrue(basePolicy == basePolicy2); + TestClassPolicy filePolicy2 = delegate.get(FILE_TYPE); + assertTrue(filePolicy == filePolicy2); + TestClassPolicy folderPolicy2 = delegate.get(FOLDER_TYPE); + assertTrue(folderPolicy == folderPolicy2); + + // Bind new behaviour (forcing base & file cache resets) + Behaviour newBaseBehaviour = new JavaBehaviour(this, "newBaseTest"); + policyComponent.bindClassBehaviour(policyName, BASE_TYPE, newBaseBehaviour); + + // Invoke Policies + TestClassPolicy basePolicy3 = delegate.get(BASE_TYPE); + assertTrue(basePolicy3 != basePolicy2); + String baseResult3 = basePolicy3.test("base"); + assertEquals("NewBase: base", baseResult3); + TestClassPolicy filePolicy3 = delegate.get(FILE_TYPE); + assertTrue(filePolicy3 != filePolicy2); + String fileResult3 = filePolicy3.test("file"); + assertEquals("NewBase: file", fileResult3); + TestClassPolicy folderPolicy3 = delegate.get(FOLDER_TYPE); + assertTrue(folderPolicy3 == folderPolicy2); + String folderResult3 = folderPolicy3.test("folder"); + assertEquals("Folder: folder", folderResult3); + + // Bind new behaviour (forcing file cache reset) + Behaviour fileBehaviour = new JavaBehaviour(this, "fileTest"); + policyComponent.bindClassBehaviour(policyName, FILE_TYPE, fileBehaviour); + + // Invoke Policies + TestClassPolicy basePolicy4 = delegate.get(BASE_TYPE); + assertTrue(basePolicy4 == basePolicy3); + String baseResult4 = basePolicy4.test("base"); + assertEquals("NewBase: base", baseResult4); + TestClassPolicy filePolicy4 = delegate.get(FILE_TYPE); + assertTrue(filePolicy4 != filePolicy3); + String fileResult4 = filePolicy4.test("file"); + assertEquals("File: file", fileResult4); + TestClassPolicy folderPolicy4 = delegate.get(FOLDER_TYPE); + assertTrue(folderPolicy4 == folderPolicy4); + String folderResult4 = folderPolicy4.test("folder"); + assertEquals("Folder: folder", folderResult4); + } + + + public void testPropertyDelegate() + { + // Register Policy + PropertyPolicyDelegate delegate = policyComponent.registerPropertyPolicy(TestPropertyPolicy.class); + + // Bind Property Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour fileBehaviour = new JavaBehaviour(this, "fileTest"); + policyComponent.bindPropertyBehaviour(policyName, FILE_TYPE, FILE_PROP_B, fileBehaviour); + + // Test NOOP Policy delegate + Collection basePolicies = delegate.getList(BASE_TYPE, BASE_PROP_A); + assertNotNull(basePolicies); + assertTrue(basePolicies.size() == 0); + TestPropertyPolicy basePolicy = delegate.get(BASE_TYPE, BASE_PROP_A); + assertNotNull(basePolicy); + + // Test single Policy delegate + Collection filePolicies = delegate.getList(FILE_TYPE, FILE_PROP_B); + assertNotNull(filePolicies); + assertTrue(filePolicies.size() == 1); + TestPropertyPolicy filePolicy = delegate.get(FILE_TYPE, FILE_PROP_B); + assertNotNull(filePolicy); + assertEquals(filePolicies.iterator().next(), filePolicy); + + // Bind Service Behaviour + Behaviour serviceBehaviour = new JavaBehaviour(this, "serviceTest"); + policyComponent.bindPropertyBehaviour(policyName, this, serviceBehaviour); + + // Test multi Policy delegate + Collection file2Policies = delegate.getList(FILE_TYPE, FILE_PROP_B); + assertNotNull(file2Policies); + assertTrue(file2Policies.size() == 2); + TestPropertyPolicy filePolicy2 = delegate.get(FILE_TYPE, FILE_PROP_B); + assertNotNull(filePolicy2); + } + + + public void testPropertyOverride() + { + // Register Policy + PropertyPolicyDelegate delegate = policyComponent.registerPropertyPolicy(TestPropertyPolicy.class); + + // Bind Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour baseBehaviour = new JavaBehaviour(this, "baseTest"); + policyComponent.bindPropertyBehaviour(policyName, BASE_TYPE, BASE_PROP_A, baseBehaviour); + Behaviour folderBehaviour = new JavaBehaviour(this, "folderTest"); + policyComponent.bindPropertyBehaviour(policyName, FOLDER_TYPE, BASE_PROP_A, folderBehaviour); + Behaviour folderBehaviourD = new JavaBehaviour(this, "folderTest"); + policyComponent.bindPropertyBehaviour(policyName, FOLDER_TYPE, FOLDER_PROP_D, folderBehaviourD); + + // Invoke Policies + TestPropertyPolicy basePolicy = delegate.get(BASE_TYPE, BASE_PROP_A); + String baseResult = basePolicy.test("base"); + assertEquals("Base: base", baseResult); + TestPropertyPolicy filePolicy = delegate.get(FILE_TYPE, BASE_PROP_A); + String fileResult = filePolicy.test("file"); + assertEquals("Base: file", fileResult); + TestPropertyPolicy folderPolicy = delegate.get(FOLDER_TYPE, BASE_PROP_A); + String folderResult = folderPolicy.test("folder"); + assertEquals("Folder: folder", folderResult); + TestPropertyPolicy folderPolicy2 = delegate.get(FOLDER_TYPE, FOLDER_PROP_D); + String folderResult2 = folderPolicy2.test("folder"); + assertEquals("Folder: folder", folderResult2); + } + + + public void testPropertyWildcard() + { + // Register Policy + PropertyPolicyDelegate delegate = policyComponent.registerPropertyPolicy(TestPropertyPolicy.class); + + // Bind Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour baseBehaviour = new JavaBehaviour(this, "baseTest"); + policyComponent.bindPropertyBehaviour(policyName, BASE_TYPE, baseBehaviour); + Behaviour folderBehaviour = new JavaBehaviour(this, "folderTest"); + policyComponent.bindPropertyBehaviour(policyName, FOLDER_TYPE, folderBehaviour); + Behaviour aspectBehaviour = new JavaBehaviour(this, "aspectTest"); + policyComponent.bindPropertyBehaviour(policyName, TEST_ASPECT, aspectBehaviour); + + // Invoke Policies + TestPropertyPolicy basePolicy = delegate.get(BASE_TYPE, BASE_PROP_A); + String baseResult = basePolicy.test("base"); + assertEquals("Base: base", baseResult); + TestPropertyPolicy filePolicy = delegate.get(FILE_TYPE, BASE_PROP_A); + String fileResult = filePolicy.test("file"); + assertEquals("Base: file", fileResult); + TestPropertyPolicy folderPolicy = delegate.get(FOLDER_TYPE, BASE_PROP_A); + String folderResult = folderPolicy.test("folder"); + assertEquals("Folder: folder", folderResult); + TestPropertyPolicy folderPolicy2 = delegate.get(FOLDER_TYPE, FOLDER_PROP_D); + String folderResult2 = folderPolicy2.test("folder"); + assertEquals("Folder: folder", folderResult2); + TestPropertyPolicy aspectPolicy = delegate.get(TEST_ASPECT, ASPECT_PROP_A); + String aspectResult = aspectPolicy.test("aspect_prop_a"); + assertEquals("Aspect: aspect_prop_a", aspectResult); + TestPropertyPolicy aspectPolicy2 = delegate.get(TEST_ASPECT, FOLDER_PROP_D); + String aspectResult2 = aspectPolicy2.test("aspect_folder_d"); + assertEquals("Aspect: aspect_folder_d", aspectResult2); + + // Override wild-card with specific property binding + Behaviour folderDBehaviour = new JavaBehaviour(this, "folderDTest"); + policyComponent.bindPropertyBehaviour(policyName, FOLDER_TYPE, FOLDER_PROP_D, folderDBehaviour); + TestPropertyPolicy folderPolicy3 = delegate.get(FOLDER_TYPE, FOLDER_PROP_D); + String folderResult3 = folderPolicy3.test("folder"); + assertEquals("FolderD: folder", folderResult3); + } + + + public void testPropertyCache() + { + // Register Policy + PropertyPolicyDelegate delegate = policyComponent.registerPropertyPolicy(TestPropertyPolicy.class); + + // Bind Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour baseBehaviour = new JavaBehaviour(this, "baseTest"); + policyComponent.bindPropertyBehaviour(policyName, BASE_TYPE, baseBehaviour); + Behaviour folderBehaviour = new JavaBehaviour(this, "folderTest"); + policyComponent.bindPropertyBehaviour(policyName, FOLDER_TYPE, folderBehaviour); + Behaviour folderDBehaviour = new JavaBehaviour(this, "folderDTest"); + policyComponent.bindPropertyBehaviour(policyName, FOLDER_TYPE, FOLDER_PROP_D, folderDBehaviour); + Behaviour aspectBehaviour = new JavaBehaviour(this, "aspectTest"); + policyComponent.bindPropertyBehaviour(policyName, TEST_ASPECT, aspectBehaviour); + + // Invoke Policies + TestPropertyPolicy filePolicy = delegate.get(FILE_TYPE, BASE_PROP_A); + String fileResult = filePolicy.test("file"); + assertEquals("Base: file", fileResult); + TestPropertyPolicy folderPolicy = delegate.get(FOLDER_TYPE, FOLDER_PROP_D); + String folderResult = folderPolicy.test("folder"); + assertEquals("FolderD: folder", folderResult); + + // Re-bind Behaviour + Behaviour newBaseBehaviour = new JavaBehaviour(this, "newBaseTest"); + policyComponent.bindPropertyBehaviour(policyName, BASE_TYPE, newBaseBehaviour); + + // Re-invoke Policies + TestPropertyPolicy filePolicy2 = delegate.get(FILE_TYPE, BASE_PROP_A); + String fileResult2 = filePolicy2.test("file"); + assertEquals("NewBase: file", fileResult2); + TestPropertyPolicy folderPolicy2 = delegate.get(FOLDER_TYPE, FOLDER_PROP_D); + String folderResult2 = folderPolicy2.test("folder"); + assertEquals("FolderD: folder", folderResult2); + } + + + public void testAssociationDelegate() + { + // Register Policy + AssociationPolicyDelegate delegate = policyComponent.registerAssociationPolicy(TestAssociationPolicy.class); + + // Bind Association Behaviour + QName policyName = QName.createQName(TEST_NAMESPACE, "test"); + Behaviour baseBehaviour = new JavaBehaviour(this, "baseTest"); + policyComponent.bindAssociationBehaviour(policyName, BASE_TYPE, BASE_ASSOC_A, baseBehaviour); + + // Test single Policy delegate + Collection filePolicies = delegate.getList(FILE_TYPE, BASE_ASSOC_A); + assertNotNull(filePolicies); + assertTrue(filePolicies.size() == 1); + TestAssociationPolicy filePolicy = delegate.get(FILE_TYPE, BASE_ASSOC_A); + assertNotNull(filePolicy); + String fileResult = filePolicy.test("file"); + assertEquals("Base: file", fileResult); + + // Bind Service Behaviour + Behaviour serviceBehaviour = new JavaBehaviour(this, "serviceTest"); + policyComponent.bindAssociationBehaviour(policyName, this, serviceBehaviour); + + // Test multi Policy delegate + Collection file2Policies = delegate.getList(FILE_TYPE, BASE_ASSOC_A); + assertNotNull(file2Policies); + assertTrue(file2Policies.size() == 2); + TestAssociationPolicy filePolicy2 = delegate.get(FILE_TYPE, BASE_ASSOC_A); + assertNotNull(filePolicy2); + } + + + // + // The following interfaces represents policies + // + + public interface TestClassPolicy extends ClassPolicy + { + static String NAMESPACE = TEST_NAMESPACE; + public String test(String argument); + } + + public interface TestPropertyPolicy extends PropertyPolicy + { + static String NAMESPACE = TEST_NAMESPACE; + public String test(String argument); + } + + public interface TestAssociationPolicy extends AssociationPolicy + { + static String NAMESPACE = TEST_NAMESPACE; + public String test(String argument); + } + + public interface InvalidMetaDataPolicy extends ClassPolicy + { + static int NAMESPACE = 0; + public String test(String nodeRef); + } + + public interface NoMethodPolicy extends ClassPolicy + { + } + + public interface MultiMethodPolicy extends ClassPolicy + { + public void a(); + public void b(); + } + + + // + // The following methods represent Java Behaviours + // + + public String validTest(String argument) + { + return "ValidTest: " + argument; + } + + public String baseTest(String argument) + { + return "Base: " + argument; + } + + public String newBaseTest(String argument) + { + return "NewBase: " + argument; + } + + public String fileTest(String argument) + { + return "File: " + argument; + } + + public String folderTest(String argument) + { + return "Folder: " + argument; + } + + public String aspectTest(String argument) + { + return "Aspect: " + argument; + } + + public String folderDTest(String argument) + { + return "FolderD: " + argument; + } + + public String serviceTest(String argument) + { + return "Service: " + argument; + } + +} diff --git a/source/java/org/alfresco/repo/policy/PolicyDefinition.java b/source/java/org/alfresco/repo/policy/PolicyDefinition.java new file mode 100644 index 0000000000..f2857dbee6 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyDefinition.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import org.alfresco.service.namespace.QName; + + +/** + * Definition of a Policy + * + * @author David Caruana + * + * @param

    the policy interface + */ +public interface PolicyDefinition

    +{ + /** + * Gets the name of the Policy + * + * @return policy name + */ + public QName getName(); + + + /** + * Gets the Policy interface class + * + * @return the class + */ + public Class

    getPolicyInterface(); + + + /** + * Gets the Policy type + * @return the policy type + */ + public PolicyType getType(); +} diff --git a/source/java/org/alfresco/repo/policy/PolicyException.java b/source/java/org/alfresco/repo/policy/PolicyException.java new file mode 100644 index 0000000000..8e085ac32f --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * Base Policy Exception. + * + * @author David Caruana + */ +public class PolicyException extends RuntimeException +{ + private static final long serialVersionUID = 3761122726173290550L; + + + public PolicyException(String msg) + { + super(msg); + } + + public PolicyException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/repo/policy/PolicyFactory.java b/source/java/org/alfresco/repo/policy/PolicyFactory.java new file mode 100644 index 0000000000..17e7cb4a4b --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyFactory.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + + +/** + * A Policy Factory is responsible for creating Policy implementations. + * + * @author David Caruana + * + * @param the type of binding + * @param

    the policy interface + */ +/*package*/ class PolicyFactory +{ + // Behaviour Index to query + private BehaviourIndex index; + + // The policy interface class + private Class

    policyClass; + + + /** + * Construct. + * + * @param policyClass the policy class + * @param index the behaviour index to query + */ + /*package*/ PolicyFactory(Class

    policyClass, BehaviourIndex index) + { + this.policyClass = policyClass; + this.index = index; + } + + + /** + * Gets the Policy class created by this factory + * + * @return the policy class + */ + protected Class

    getPolicyClass() + { + return policyClass; + } + + + /** + * Construct a Policy implementation for the specified binding + * + * @param binding the binding + * @return the policy implementation + */ + public P create(B binding) + { + Collection

    policyInterfaces = createList(binding); + return toPolicy(policyInterfaces); + } + + + /** + * Construct a collection of Policy implementations for the specified binding + * + * @param binding the binding + * @return the collection of policy implementations + */ + @SuppressWarnings("unchecked") + public Collection

    createList(B binding) + { + Collection behaviourDefs = index.find(binding); + List

    policyInterfaces = new ArrayList

    (behaviourDefs.size()); + for (BehaviourDefinition behaviourDef : behaviourDefs) + { + Behaviour behaviour = behaviourDef.getBehaviour(); + P policyIF = behaviour.getInterface(policyClass); + policyInterfaces.add(policyIF); + } + + return policyInterfaces; + } + + + /** + * Construct a single aggregate policy implementation for the specified + * collection of policy implementations. + * + * @param policyList the policy implementations to aggregate + * @return the aggregate policy implementation + */ + @SuppressWarnings("unchecked") + public P toPolicy(Collection

    policyList) + { + if (policyList.size() == 1) + { + return policyList.iterator().next(); + } + else if (policyList.size() == 0) + { + return (P)Proxy.newProxyInstance(policyClass.getClassLoader(), + new Class[]{policyClass}, new NOOPHandler()); + } + else + { + return (P)Proxy.newProxyInstance(policyClass.getClassLoader(), + new Class[]{policyClass, PolicyList.class}, new MultiHandler

    (policyList)); + } + } + + + /** + * NOOP Invocation Handler. + * + * @author David Caruana + * + */ + private static class NOOPHandler implements InvocationHandler + { + /* (non-Javadoc) + * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) + */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + if (method.getName().equals("toString")) + { + return toString(); + } + else if (method.getName().equals("hashCode")) + { + return hashCode(); + } + else if (method.getName().equals("equals")) + { + return equals(args[0]); + } + return null; + } + } + + + /** + * Multi-policy Invocation Handler. + * + * @author David Caruana + * + * @param

    policy interface + */ + @SuppressWarnings("hiding") + private static class MultiHandler

    implements InvocationHandler, PolicyList + { + private Collection

    policyInterfaces; + + /** + * Construct + * + * @param policyInterfaces the collection of policy implementations + */ + public MultiHandler(Collection

    policyInterfaces) + { + this.policyInterfaces = Collections.unmodifiableCollection(policyInterfaces); + } + + /* (non-Javadoc) + * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) + */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + // Handle PolicyList level methods + if (method.getDeclaringClass().equals(PolicyList.class)) + { + return method.invoke(this, args); + } + + // Handle Object level methods + if (method.getName().equals("toString")) + { + return toString() + ": wrapped " + policyInterfaces.size() + " policies"; + } + else if (method.getName().equals("hashCode")) + { + return hashCode(); + } + else if (method.getName().equals("equals")) + { + return equals(args[0]); + } + + // Invoke each wrapped policy in turn + try + { + Object result = null; + for (P policyInterface : policyInterfaces) + { + result = method.invoke(policyInterface, args); + } + return result; + } + catch (InvocationTargetException e) + { + throw e.getCause(); + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.PolicyList#getPolicies() + */ + public Collection getPolicies() + { + return policyInterfaces; + } + } + +} diff --git a/source/java/org/alfresco/repo/policy/PolicyList.java b/source/java/org/alfresco/repo/policy/PolicyList.java new file mode 100644 index 0000000000..a34efd686e --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyList.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.Collection; + +/** + * @author David Caruana + */ +/*package*/ interface PolicyList

    +{ + /** + * @return the set of policies within this policy set + */ + public Collection

    getPolicies(); +} diff --git a/source/java/org/alfresco/repo/policy/PolicyScope.java b/source/java/org/alfresco/repo/policy/PolicyScope.java new file mode 100644 index 0000000000..72a34a36c6 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyScope.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.namespace.QName; + +/** + * Policy scope. + *

    + * Helper often used by policies which require information + * about a node to be gathered, for example onCopy or onCreateVersion. + * + * @author Roy Wetherall + */ +public class PolicyScope extends AspectDetails +{ + /** + * The aspects + */ + protected Map aspectCopyDetails = new HashMap(); + + /** + * Constructor + * + * @param classRef the class reference + */ + public PolicyScope(QName classRef) + { + super(classRef); + } + + /** + * Add a property + * + * @param classRef the class reference + * @param qName the qualified name of the property + * @param value the value of the property + */ + public void addProperty(QName classRef, QName qName, Serializable value) + { + if (classRef.equals(this.classRef) == true) + { + addProperty(qName, value); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails == null) + { + // Add the aspect + aspectDetails = addAspect(classRef); + } + aspectDetails.addProperty(qName, value); + } + } + + /** + * Removes a property from the list + * + * @param classRef the class reference + * @param qName the qualified name + */ + public void removeProperty(QName classRef, QName qName) + { + if (classRef.equals(this.classRef) == true) + { + removeProperty(qName); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails != null) + { + aspectDetails.removeProperty(qName); + } + } + } + + /** + * Get the properties + * + * @param classRef the class ref + * @return the properties that should be copied + */ + public Map getProperties(QName classRef) + { + Map result = null; + if (classRef.equals(this.classRef) == true) + { + result = getProperties(); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails != null) + { + result = aspectDetails.getProperties(); + } + } + + return result; + } + + /** + * Adds a child association + * + * @param classRef + * @param qname + * @param childAssocRef + */ + public void addChildAssociation(QName classRef, ChildAssociationRef childAssocRef) + { + if (classRef.equals(this.classRef) == true) + { + addChildAssociation(childAssocRef); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails == null) + { + // Add the aspect + aspectDetails = addAspect(classRef); + } + aspectDetails.addChildAssociation(childAssocRef); + } + } + + /** + * + * @param classRef + * @param childAssocRef + * @param alwaysTraverseAssociation + */ + public void addChildAssociation(QName classRef, ChildAssociationRef childAssocRef, boolean alwaysTraverseAssociation) + { + if (classRef.equals(this.classRef) == true) + { + addChildAssociation(childAssocRef, alwaysTraverseAssociation); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails == null) + { + // Add the aspect + aspectDetails = addAspect(classRef); + } + aspectDetails.addChildAssociation(childAssocRef, alwaysTraverseAssociation); + } + } + + /** + * Get a child association + * + * @param classRef + * @return + */ + public List getChildAssociations(QName classRef) + { + List result = null; + if (classRef.equals(this.classRef) == true) + { + result = getChildAssociations(); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails != null) + { + result = aspectDetails.getChildAssociations(); + } + } + + return result; + } + + public boolean isChildAssociationRefAlwaysTraversed(QName classRef, ChildAssociationRef childAssocRef) + { + boolean result = false; + if (classRef.equals(this.classRef) == true) + { + result = isChildAssociationRefAlwaysTraversed(childAssocRef); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails != null) + { + result = aspectDetails.isChildAssociationRefAlwaysTraversed(childAssocRef); + } + } + + return result; + } + + /** + * Add an association + * + * @param classRef + * @param qname + * @param nodeAssocRef + */ + public void addAssociation(QName classRef, AssociationRef nodeAssocRef) + { + if (classRef.equals(this.classRef) == true) + { + addAssociation(nodeAssocRef); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails == null) + { + // Add the aspect + aspectDetails = addAspect(classRef); + } + aspectDetails.addAssociation(nodeAssocRef); + } + } + + + + /** + * Get associations + * + * @param classRef + * @return + */ + public List getAssociations(QName classRef) + { + List result = null; + if (classRef.equals(this.classRef) == true) + { + result = getAssociations(); + } + else + { + AspectDetails aspectDetails = this.aspectCopyDetails.get(classRef); + if (aspectDetails != null) + { + result = aspectDetails.getAssociations(); + } + } + + return result; + } + + /** + * Add an aspect + * + * @param aspect the aspect class reference + * @return the apsect copy details (returned as a helper) + */ + public AspectDetails addAspect(QName aspect) + { + AspectDetails result = new AspectDetails(aspect); + this.aspectCopyDetails.put(aspect, result); + return result; + } + + /** + * Removes an aspect from the list + * + * @param aspect the aspect class reference + */ + public void removeAspect(QName aspect) + { + this.aspectCopyDetails.remove(aspect); + } + + /** + * Gets a list of the aspects + * + * @return a list of aspect to copy + */ + public Set getAspects() + { + return this.aspectCopyDetails.keySet(); + } +} + +/** + * Aspect details class. + *

    + * Contains the details of an aspect this can be used for copying or versioning. + * + * @author Roy Wetherall + */ +/*package*/ class AspectDetails +{ + /** + * The properties + */ + protected Map properties = new HashMap(); + + /** + * The child associations + */ + protected List childAssocs = new ArrayList(); + + /** + * The target associations + */ + protected List targetAssocs = new ArrayList(); + + /** + * The class ref of the aspect + */ + protected QName classRef; + + /** + * Map of assocs that will always be traversed + */ + protected Map alwaysTraverseMap = new HashMap(); + + /** + * Constructor + * + * @param classRef the class ref + */ + public AspectDetails(QName classRef) + { + this.classRef = classRef; + } + + /** + * Add a property to the list + * + * @param qName the qualified name of the property + * @param value the value of the property + */ + public void addProperty(QName qName, Serializable value) + { + this.properties.put(qName, value); + } + + /** + * Remove a property from the list + * + * @param qName the qualified name of the property + */ + public void removeProperty(QName qName) + { + this.properties.remove(qName); + } + + /** + * Gets the map of properties + * + * @return map of property names and values + */ + public Map getProperties() + { + return properties; + } + + /** + * Add a child association + * + * @param childAssocRef the child association reference + */ + protected void addChildAssociation(ChildAssociationRef childAssocRef) + { + this.childAssocs.add(childAssocRef); + } + + /** + * Add a child association + * + * @param childAssocRef the child assoc reference + * @param alwaysDeepCopy indicates whether the assoc should always be traversed + */ + protected void addChildAssociation(ChildAssociationRef childAssocRef, boolean alwaysTraverseAssociation) + { + addChildAssociation(childAssocRef); + + if (alwaysTraverseAssociation == true) + { + // Add to the list of deep copy child associations + this.alwaysTraverseMap.put(childAssocRef, childAssocRef); + } + } + + /** + * Indicates whether a child association ref is always traversed or not + * + * @param childAssocRef the child association reference + * @return true if the assoc is always traversed, false otherwise + */ + protected boolean isChildAssociationRefAlwaysTraversed(ChildAssociationRef childAssocRef) + { + return this.alwaysTraverseMap.containsKey(childAssocRef); + } + + /** + * Gets the child associations to be copied + * + * @return map containing the child associations to be copied + */ + public List getChildAssociations() + { + return this.childAssocs; + } + + /** + * Adds an association to be copied + * + * @param qname the qualified name of the association + * @param nodeAssocRef the association reference + */ + protected void addAssociation(AssociationRef nodeAssocRef) + { + this.targetAssocs.add(nodeAssocRef); + } + + /** + * Gets the map of associations to be copied + * + * @return a map conatining the associations to be copied + */ + public List getAssociations() + { + return this.targetAssocs; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/policy/PolicyType.java b/source/java/org/alfresco/repo/policy/PolicyType.java new file mode 100644 index 0000000000..68c7ed8b05 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PolicyType.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * Type of Policy. + * + * @author David Caruana + * + */ +public enum PolicyType +{ + Class, + Property, + Association +}; diff --git a/source/java/org/alfresco/repo/policy/PropertyPolicy.java b/source/java/org/alfresco/repo/policy/PropertyPolicy.java new file mode 100644 index 0000000000..5d634cff29 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PropertyPolicy.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +/** + * Marker interface for representing a Property-level Policy. + * + * @author David Caruana + */ +public interface PropertyPolicy extends Policy +{ +} diff --git a/source/java/org/alfresco/repo/policy/PropertyPolicyDelegate.java b/source/java/org/alfresco/repo/policy/PropertyPolicyDelegate.java new file mode 100644 index 0000000000..7adfe84a38 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/PropertyPolicyDelegate.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Delegate for a Class Feature-level (Property and Association) Policies. Provides + * access to Policy Interface implementations which invoke the appropriate bound behaviours. + * + * @author David Caruana + * + * @param

    the policy interface + */ +public class PropertyPolicyDelegate

    +{ + private DictionaryService dictionary; + private CachedPolicyFactory factory; + + + /** + * Construct. + * + * @param dictionary the dictionary service + * @param policyClass the policy interface class + * @param index the behaviour index to query against + */ + @SuppressWarnings("unchecked") + /*package*/ PropertyPolicyDelegate(DictionaryService dictionary, Class

    policyClass, BehaviourIndex index) + { + // Get list of all pre-registered behaviours for the policy and + // ensure they are valid. + Collection definitions = index.getAll(); + for (BehaviourDefinition definition : definitions) + { + definition.getBehaviour().getInterface(policyClass); + } + + // Rely on cached implementation of policy factory + // Note: Could also use PolicyFactory (without caching) + this.factory = new CachedPolicyFactory(policyClass, index); + this.dictionary = dictionary; + } + + /** + * Ensures the validity of the given property type + * + * @param assocTypeQName + * @throws IllegalArgumentException + */ + private void checkPropertyType(QName propertyQName) throws IllegalArgumentException + { + PropertyDefinition propertyDef = dictionary.getProperty(propertyQName); + if (propertyDef == null) + { + throw new IllegalArgumentException("Property " + propertyQName + " has not been defined in the data dictionary"); + } + } + + + /** + * Gets the Policy implementation for the specified Class and Propery + * + * When multiple behaviours are bound to the policy for the class feature, an + * aggregate policy implementation is returned which invokes each policy + * in turn. + * + * @param classQName the class qualified name + * @param propertyQName the property qualified name + * @return the policy + */ + public P get(QName classQName, QName propertyQName) + { + return get(null, classQName, propertyQName); + } + + /** + * Gets the Policy implementation for the specified Class and Propery + * + * When multiple behaviours are bound to the policy for the class feature, an + * aggregate policy implementation is returned which invokes each policy + * in turn. + * + * @param nodeRef the node reference + * @param classQName the class qualified name + * @param propertyQName the property qualified name + * @return the policy + */ + public P get(NodeRef nodeRef, QName classQName, QName propertyQName) + { + checkPropertyType(propertyQName); + return factory.create(new ClassFeatureBehaviourBinding(dictionary, nodeRef, classQName, propertyQName)); + } + + /** + * Gets the collection of Policy implementations for the specified Class and Property + * + * @param classQName the class qualified name + * @param propertyQName the property qualified name + * @return the collection of policies + */ + public Collection

    getList(QName classQName, QName propertyQName) + { + return getList(null, classQName, propertyQName); + } + + /** + * Gets the collection of Policy implementations for the specified Class and Property + * + * @param nodeRef the node reference + * @param classQName the class qualified name + * @param propertyQName the property qualified name + * @return the collection of policies + */ + public Collection

    getList(NodeRef nodeRef, QName classQName, QName propertyQName) + { + checkPropertyType(propertyQName); + return factory.createList(new ClassFeatureBehaviourBinding(dictionary, nodeRef, classQName, propertyQName)); + } + + /** + * Gets a Policy for all the given Class and Property + * + * @param classQNames the class qualified names + * @param propertyQName the property qualified name + * @return Return the policy + */ + public P get(Set classQNames, QName propertyQName) + { + return get(null, classQNames, propertyQName); + } + + /** + * Gets a Policy for all the given Class and Property + * + * @param nodeRef the node reference + * @param classQNames the class qualified names + * @param propertyQName the property qualified name + * @return Return the policy + */ + public P get(NodeRef nodeRef, Set classQNames, QName propertyQName) + { + checkPropertyType(propertyQName); + return factory.toPolicy(getList(nodeRef, classQNames, propertyQName)); + } + + /** + * Gets the Policy instances for all the given Classes and Properties + * + * @param classQNames the class qualified names + * @param propertyQName the property qualified name + * @return Return the policies + */ + public Collection

    getList(Set classQNames, QName propertyQName) + { + return getList(null, classQNames, propertyQName); + } + + /** + * Gets the Policy instances for all the given Classes and Properties + * + * @param nodeRef the node reference + * @param classQNames the class qualified names + * @param propertyQName the property qualified name + * @return Return the policies + */ + public Collection

    getList(NodeRef nodeRef, Set classQNames, QName propertyQName) + { + checkPropertyType(propertyQName); + Collection

    policies = new HashSet

    (); + for (QName classQName : classQNames) + { + P policy = factory.create(new ClassFeatureBehaviourBinding(dictionary, nodeRef, classQName, propertyQName)); + if (policy instanceof PolicyList) + { + policies.addAll(((PolicyList

    )policy).getPolicies()); + } + else + { + policies.add(policy); + } + } + return policies; + } + +} diff --git a/source/java/org/alfresco/repo/policy/ServiceBehaviourBinding.java b/source/java/org/alfresco/repo/policy/ServiceBehaviourBinding.java new file mode 100644 index 0000000000..aa8d22c8bf --- /dev/null +++ b/source/java/org/alfresco/repo/policy/ServiceBehaviourBinding.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.policy; + + +/** + * Behaviour binding to a Service. + * + * @author David Caruana + * + */ +public class ServiceBehaviourBinding implements BehaviourBinding +{ + // The service + private Object service; + + /** + * Construct + * + * @param service the service + */ + /*package*/ ServiceBehaviourBinding(Object service) + { + this.service = service; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.policy.BehaviourBinding#generaliseBinding() + */ + public BehaviourBinding generaliseBinding() + { + return null; + } + + /** + * Gets the Service + * + * @return the service + */ + public Object getService() + { + return service; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || !(obj instanceof ServiceBehaviourBinding)) + { + return false; + } + return service.equals(((ServiceBehaviourBinding)obj).service); + } + + @Override + public int hashCode() + { + return service.hashCode(); + } + + @Override + public String toString() + { + return "ServiceBinding[service=" + this + "]"; + } + +} diff --git a/source/java/org/alfresco/repo/policy/policycomponenttest_model.xml b/source/java/org/alfresco/repo/policy/policycomponenttest_model.xml new file mode 100644 index 0000000000..f0268dda82 --- /dev/null +++ b/source/java/org/alfresco/repo/policy/policycomponenttest_model.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + d:text + + + + + + test:base + + + + + + + test:base + + + d:text + + + d:text + + + + + an overriden default value + + + + + + test:base + + + d:text + + + + + + test:folder + + + + + + + + + + + + d:int + + + + + + diff --git a/source/java/org/alfresco/repo/rule/BaseRuleTest.java b/source/java/org/alfresco/repo/rule/BaseRuleTest.java new file mode 100644 index 0000000000..061f83eaa5 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/BaseRuleTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuter; +import org.alfresco.repo.configuration.ConfigurableService; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; + +/** + * Base class for rule service test. + *

    + * This file contains a number of helpers to reduce the duplication in tests. + * + * @author Roy Wetherall + */ +public class BaseRuleTest extends BaseSpringTest +{ + /** + * Data used in the tests + */ + protected static final String RULE_TYPE_NAME = RuleType.INBOUND; + + /** + * Action used in tests + */ + protected static final String ACTION_DEF_NAME = AddFeaturesActionExecuter.NAME; + protected static final String ACTION_PROP_NAME_1 = AddFeaturesActionExecuter.PARAM_ASPECT_NAME; + protected static final QName ACTION_PROP_VALUE_1 = ContentModel.ASPECT_LOCKABLE; + + /** + * ActionCondition used in tests + */ + protected static final String CONDITION_DEF_NAME = ComparePropertyValueEvaluator.NAME; + protected static final String COND_PROP_NAME_1 = ComparePropertyValueEvaluator.PARAM_VALUE; + protected static final String COND_PROP_VALUE_1 = ".doc"; + + /** + * Rule values used in tests + */ + protected static final String TITLE = "title"; + protected static final String DESCRIPTION = "description"; + + /** + * Services + */ + protected NodeService nodeService; + protected ContentService contentService; + protected RuleService ruleService; + protected ConfigurableService configService; + protected AuthenticationComponent authenticationComponent; + + /** + * Rule type used in tests + */ + protected RuleType ruleType; + + /** + * Store and node references + */ + protected StoreRef testStoreRef; + protected NodeRef rootNodeRef; + protected NodeRef nodeRef; + protected NodeRef configFolder; + protected ActionService actionService; + protected TransactionService transactionService; + + /** + * onSetUpInTransaction implementation + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + // Get the services + this.nodeService = (NodeService) this.applicationContext + .getBean("nodeService"); + this.contentService = (ContentService) this.applicationContext + .getBean("contentService"); + this.ruleService = (RuleService) this.applicationContext + .getBean("ruleService"); + this.configService = (ConfigurableService)this.applicationContext + .getBean("configurableService"); + this.actionService = (ActionService)this.applicationContext.getBean("actionService"); + this.transactionService = (TransactionService)this.applicationContext.getBean("transactionComponent"); + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + + authenticationComponent.setSystemUserAsCurrentUser(); + + // Get the rule type + this.ruleType = this.ruleService.getRuleType(RULE_TYPE_NAME); + + // Create the store and get the root node + this.testStoreRef = this.nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, "Test_" + + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode(rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTAINER).getChildRef(); + } + + @Override + protected void onTearDownInTransaction() + { + authenticationComponent.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + + protected void addRulesAspect() + { + // Make the node actionable + this.configService.makeConfigurable(this.nodeRef); + this.nodeService.addAspect(this.nodeRef, RuleModel.ASPECT_RULES, null); + } + + protected Rule createTestRule() + { + return createTestRule(false); + } + + protected Rule createTestRule(boolean isAppliedToChildren) + { + // Rule properties + Map conditionProps = new HashMap(); + conditionProps.put(COND_PROP_NAME_1, COND_PROP_VALUE_1); + + Map actionProps = new HashMap(); + actionProps.put(ACTION_PROP_NAME_1, ACTION_PROP_VALUE_1); + + // Create the rule + Rule rule = this.ruleService.createRule(this.ruleType.getName()); + rule.setTitle(TITLE); + rule.setDescription(DESCRIPTION); + rule.applyToChildren(isAppliedToChildren); + + ActionCondition actionCondition = this.actionService.createActionCondition(CONDITION_DEF_NAME); + actionCondition.setParameterValues(conditionProps); + rule.addActionCondition(actionCondition); + + Action action = this.actionService.createAction(CONDITION_DEF_NAME); + action.setParameterValues(conditionProps); + rule.addAction(action); + + return rule; + } + + protected void checkRule(RuleImpl rule, String id) + { + // Check the basic details of the rule + assertEquals(id, rule.getId()); + assertEquals(this.ruleType.getName(), rule.getRuleTypeName()); + assertEquals(TITLE, rule.getTitle()); + assertEquals(DESCRIPTION, rule.getDescription()); + + // Check conditions + List ruleConditions = rule.getActionConditions(); + assertNotNull(ruleConditions); + assertEquals(1, ruleConditions.size()); + assertEquals(CONDITION_DEF_NAME, ruleConditions.get(0) + .getActionConditionDefinitionName()); + Map condParams = ruleConditions.get(0) + .getParameterValues(); + assertNotNull(condParams); + assertEquals(1, condParams.size()); + assertTrue(condParams.containsKey(COND_PROP_NAME_1)); + assertEquals(COND_PROP_VALUE_1, condParams.get(COND_PROP_NAME_1)); + + // Check the actions + List ruleActions = rule.getActions(); + assertNotNull(ruleActions); + assertEquals(1, ruleActions.size()); + assertEquals(ACTION_DEF_NAME, ruleActions.get(0).getActionDefinitionName()); + Map actionParams = ruleActions.get(0).getParameterValues(); + assertNotNull(actionParams); + assertEquals(1, actionParams.size()); + assertTrue(actionParams.containsKey(ACTION_PROP_NAME_1)); + assertEquals(ACTION_PROP_VALUE_1, actionParams.get(ACTION_PROP_NAME_1)); + } +} diff --git a/source/java/org/alfresco/repo/rule/RuleCache.java b/source/java/org/alfresco/repo/rule/RuleCache.java new file mode 100644 index 0000000000..2efbfca7cb --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleCache.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; + +/** + * Rule cache interface + * + * @author Roy Wetherall + */ +public interface RuleCache +{ + List getRules(NodeRef nodeRef); + + List getInheritedRules(NodeRef nodeRef); + + void setRules(NodeRef nodeRef, List rules); + + void setInheritedRules(NodeRef nodeRef, List rules); + + void dirtyRules(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/repo/rule/RuleImpl.java b/source/java/org/alfresco/repo/rule/RuleImpl.java new file mode 100644 index 0000000000..2bc76da74f --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleImpl.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.io.Serializable; + +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.util.ParameterCheck; + +/** + * Rule implementation class. + *

    + * Encapsulates all the information about a rule. Can be creted or editied and + * then passed to the rule service to create/update a rule instance. + * + * @author Roy Wetherall + */ +public class RuleImpl extends CompositeActionImpl implements Serializable, Rule +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3544385898889097524L; + + /** + * The rule type name + */ + private String ruleTypeName; + + /** + * Indicates whether the rule is applied to all the children of the associated node + * rather than just the node itself. + */ + private boolean isAppliedToChildren = false; + + /** + * Constructor + * + * @param ruleTypeName the rule type name + */ + public RuleImpl(String id, String ruleTypeName, NodeRef owningNodeRef) + { + super(id, owningNodeRef); + ParameterCheck.mandatory("ruleTypeName", ruleTypeName); + + this.ruleTypeName = ruleTypeName; + } + + /** + * @see org.alfresco.service.cmr.rule.Rule#isAppliedToChildren() + */ + public boolean isAppliedToChildren() + { + return this.isAppliedToChildren; + } + + /** + *@see org.alfresco.service.cmr.rule.Rule#applyToChildren(boolean) + */ + public void applyToChildren(boolean isAppliedToChildren) + { + this.isAppliedToChildren = isAppliedToChildren; + } + + /** + * @see org.alfresco.service.cmr.rule.Rule#getRuleTypeName() + */ + public String getRuleTypeName() + { + return this.ruleTypeName; + } +} + diff --git a/source/java/org/alfresco/repo/rule/RuleModel.java b/source/java/org/alfresco/repo/rule/RuleModel.java new file mode 100644 index 0000000000..6f2da46721 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleModel.java @@ -0,0 +1,21 @@ +package org.alfresco.repo.rule; + +import org.alfresco.service.namespace.QName; + +/** + * Interface containing rule model constants + * + * @author Roy Wetherall + */ +public interface RuleModel +{ + /** Rule model constants */ + static final String RULE_MODEL_URI = "http://www.alfresco.org/model/rule/1.0"; + static final String RULE_MODEL_PREFIX = "rule"; + static final QName TYPE_RULE = QName.createQName(RULE_MODEL_URI, "rule"); + static final QName PROP_RULE_TYPE = QName.createQName(RULE_MODEL_URI, "ruleType"); + static final QName TYPE_RULE_CONTENT = QName.createQName(RULE_MODEL_URI, "rulecontent"); + static final QName PROP_APPLY_TO_CHILDREN = QName.createQName(RULE_MODEL_URI, "applyToChildren"); + static final QName ASPECT_RULES = QName.createQName(RULE_MODEL_URI, "rules"); + static final QName ASSOC_RULE_FOLDER = QName.createQName(RULE_MODEL_URI, "ruleFolder"); +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java b/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java new file mode 100644 index 0000000000..3e689ba30b --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java @@ -0,0 +1,1417 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionServiceImplTest; +import org.alfresco.repo.action.ActionServiceImplTest.AsyncTest; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.evaluator.InCategoryEvaluator; +import org.alfresco.repo.action.evaluator.NoConditionEvaluator; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuter; +import org.alfresco.repo.action.executer.CheckInActionExecuter; +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.repo.action.executer.CopyActionExecuter; +import org.alfresco.repo.action.executer.ImageTransformActionExecuter; +import org.alfresco.repo.action.executer.LinkCategoryActionExecuter; +import org.alfresco.repo.action.executer.MailActionExecuter; +import org.alfresco.repo.action.executer.MoveActionExecuter; +import org.alfresco.repo.action.executer.SimpleWorkflowActionExecuter; +import org.alfresco.repo.action.executer.TransformActionExecuter; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.content.transform.ContentTransformerRegistry; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.StopWatch; + +/** + * @author Roy Wetherall + */ +public class RuleServiceCoverageTest extends TestCase +{ + private static final ContentData CONTENT_DATA_TEXT = new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, "UTF-8"); + + /** + * Application context used during the test + */ + static ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:alfresco/application-context.xml"); + + /** + * Services used during the tests + */ + private TransactionService transactionService; + private RuleService ruleService; + private NodeService nodeService; + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + private NodeRef nodeRef; + private CheckOutCheckInService cociService; + private LockService lockService; + private ContentService contentService; + private ServiceRegistry serviceRegistry; + private DictionaryDAO dictionaryDAO; + private ActionService actionService; + private ContentTransformerRegistry transformerRegistry; + private CopyService copyService; + + /** + * Category related values + */ + private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/rulesystemtest"; + private static final QName CAT_PROP_QNAME = QName.createQName(TEST_NAMESPACE, "region"); + private QName regionCategorisationQName; + private NodeRef catContainer; + private NodeRef catRoot; + private NodeRef catRBase; + private NodeRef catROne; + private NodeRef catRTwo; + @SuppressWarnings("unused") + private NodeRef catRThree; + + /** + * Standard content text + */ + private static final String STANDARD_TEXT_CONTENT = "standardTextContent"; + + /** + * Setup method + */ + @Override + protected void setUp() throws Exception + { + // Get the required services + this.serviceRegistry = (ServiceRegistry)applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + this.nodeService = serviceRegistry.getNodeService(); + this.ruleService = serviceRegistry.getRuleService(); + this.cociService = serviceRegistry.getCheckOutCheckInService(); + this.lockService = serviceRegistry.getLockService(); + this.copyService = serviceRegistry.getCopyService(); + this.contentService = serviceRegistry.getContentService(); + this.dictionaryDAO = (DictionaryDAO)applicationContext.getBean("dictionaryDAO"); + this.actionService = serviceRegistry.getActionService(); + this.transactionService = serviceRegistry.getTransactionService(); + this.transformerRegistry = (ContentTransformerRegistry)applicationContext.getBean("contentTransformerRegistry"); + + AuthenticationComponent authenticationComponent = (AuthenticationComponent)applicationContext.getBean("authenticationComponent"); + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + + this.testStoreRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + + // Create the node used for tests + this.nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + // Create and authenticate the user used in the tests + //TestWithUserUtils.createUser(USER_NAME, PWD, this.rootNodeRef, this.nodeService, this.authenticationService); + //TestWithUserUtils.authenticateUser(USER_NAME, PWD, this.rootNodeRef, this.authenticationService); + } + + private Rule createRule( + String ruleTypeName, + String actionName, + Map actionParams, + String conditionName, + Map conditionParams) + { + Rule rule = this.ruleService.createRule(ruleTypeName); + ActionCondition condition = this.actionService.createActionCondition(conditionName, conditionParams); + rule.addActionCondition(condition); + Action action = this.actionService.createAction(actionName, actionParams); + rule.addAction(action); + return rule; + } + + /** + * Create the categories used in the tests + */ + private void createTestCategories() + { + // Create the test model + M2Model model = M2Model.createModel("test:rulecategory"); + model.createNamespace(TEST_NAMESPACE, "test"); + model.createImport(NamespaceService.DICTIONARY_MODEL_1_0_URI, "d"); + model.createImport(NamespaceService.CONTENT_MODEL_1_0_URI, NamespaceService.CONTENT_MODEL_PREFIX); + + // Create the region category + regionCategorisationQName = QName.createQName(TEST_NAMESPACE, "Region"); + M2Aspect generalCategorisation = model.createAspect("test:" + regionCategorisationQName.getLocalName()); + generalCategorisation.setParentName("cm:" + ContentModel.ASPECT_CLASSIFIABLE.getLocalName()); + M2Property genCatProp = generalCategorisation.createProperty("test:region"); + genCatProp.setIndexed(true); + genCatProp.setIndexedAtomically(true); + genCatProp.setMandatory(true); + genCatProp.setMultiValued(false); + genCatProp.setStoredInIndex(true); + genCatProp.setTokenisedInIndex(true); + genCatProp.setType("d:" + DataTypeDefinition.CATEGORY.getLocalName()); + + // Save the mode + dictionaryDAO.putModel(model); + + // Create the category value container and root + catContainer = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName(TEST_NAMESPACE, "categoryContainer"), ContentModel.TYPE_CONTAINER).getChildRef(); + catRoot = nodeService.createNode(catContainer, ContentModel.ASSOC_CHILDREN, QName.createQName(TEST_NAMESPACE, "categoryRoot"), ContentModel.TYPE_CATEGORYROOT).getChildRef(); + + // Create the category values + catRBase = nodeService.createNode(catRoot, ContentModel.ASSOC_CATEGORIES, QName.createQName(TEST_NAMESPACE, "Region"), ContentModel.TYPE_CATEGORY).getChildRef(); + catROne = nodeService.createNode(catRBase, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "Europe"), ContentModel.TYPE_CATEGORY).getChildRef(); + catRTwo = nodeService.createNode(catRBase, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "RestOfWorld"), ContentModel.TYPE_CATEGORY).getChildRef(); + catRThree = nodeService.createNode(catRTwo, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "US"), ContentModel.TYPE_CATEGORY).getChildRef(); + } + + /** + * Asynchronous rule tests + */ + + /** + * Check async rule execution + */ + public void testAsyncRuleExecution() + { + final NodeRef newNodeRef = TransactionUtil.executeInUserTransaction( + this.transactionService, + new TransactionUtil.TransactionWork() + { + public NodeRef doWork() + { + RuleServiceCoverageTest.this.nodeService.addAspect( + RuleServiceCoverageTest.this.nodeRef, + ContentModel.ASPECT_LOCKABLE, + null); + + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + rule.setExecuteAsynchronously(true); + + RuleServiceCoverageTest.this.ruleService.saveRule(RuleServiceCoverageTest.this.nodeRef, rule); + + NodeRef newNodeRef = RuleServiceCoverageTest.this.nodeService.createNode( + RuleServiceCoverageTest.this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + + return newNodeRef; + } + }); + + ActionServiceImplTest.postAsyncActionTest( + this.transactionService, + 1000, + 10, + new AsyncTest() + { + public boolean executeTest() + { + return RuleServiceCoverageTest.this.nodeService.hasAspect( + newNodeRef, + ContentModel.ASPECT_VERSIONABLE); + }; + }); + } + + // TODO check compensating action execution + + /** + * Standard rule coverage tests + */ + + /** + * Test: + * rule type: inbound + * condition: no-condition() + * action: add-features( + * aspect-name = versionable) + */ + public void testAddFeaturesAction() + { + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, null); + + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + assertTrue(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + Map params2 = new HashMap(2); + params2.put(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_SIMPLE_WORKFLOW); + params2.put(ContentModel.PROP_APPROVE_STEP.toString(), "approveStep"); + params2.put(ContentModel.PROP_APPROVE_MOVE.toString(), false); + + // Test that rule can be updated and execute correctly + rule.removeAllActions(); + Action action2 = this.actionService.createAction(AddFeaturesActionExecuter.NAME, params2); + rule.addAction(action2); + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef2); + assertTrue(this.nodeService.hasAspect(newNodeRef2, ContentModel.ASPECT_SIMPLE_WORKFLOW)); + assertEquals("approveStep", this.nodeService.getProperty(newNodeRef2, ContentModel.PROP_APPROVE_STEP)); + assertEquals(false, this.nodeService.getProperty(newNodeRef2, ContentModel.PROP_APPROVE_MOVE)); + + // System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + public void testDisableRule() + { + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, null); + + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + this.ruleService.disableRule(rule); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + assertFalse(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.ruleService.enableRule(rule); + + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef2); + assertTrue(this.nodeService.hasAspect(newNodeRef2, ContentModel.ASPECT_VERSIONABLE)); + + } + + public void testAddFeaturesToAFolder() + { + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_TEMPLATABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_FOLDER).getChildRef(); + + assertTrue(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_TEMPLATABLE)); + + // System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + public void testCopyFolderToTriggerRules() + { + // Create the folders and content + NodeRef copyToFolder = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}copyToFolder"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef folderToCopy = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}folderToCopy"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef contentToCopy = this.nodeService.createNode( + folderToCopy, + ContentModel.ASSOC_CONTAINS, + QName.createQName("{test}contentToCopy"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(contentToCopy); + + // Create the rule and add to folder + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_TEMPLATABLE); + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + rule.applyToChildren(true); + this.ruleService.saveRule(copyToFolder, rule); + + // Copy the folder in order to try and trigger the rule + NodeRef copiedFolder = this.copyService.copy(folderToCopy, copyToFolder, ContentModel.ASSOC_CONTAINS, QName.createQName("{test}coppiedFolder"), true); + assertNotNull(copiedFolder); + + // Check that the rule has been applied to the copied folder and content + assertTrue(this.nodeService.hasAspect(copiedFolder, ContentModel.ASPECT_TEMPLATABLE)); + for (ChildAssociationRef childAssoc : this.nodeService.getChildAssocs(copiedFolder)) + { + assertTrue(this.nodeService.hasAspect(childAssoc.getChildRef(), ContentModel.ASPECT_TEMPLATABLE)); + } + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + private Map getContentProperties() + { + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + return properties; + } + + /** + * Test: + * rule type: inbound + * condition: no-condition + * action: simple-workflow + */ + public void testSimpleWorkflowAction() + { + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, null); + + Map params = new HashMap(1); + params.put(SimpleWorkflowActionExecuter.PARAM_APPROVE_STEP, "approveStep"); + params.put(SimpleWorkflowActionExecuter.PARAM_APPROVE_FOLDER, this.rootNodeRef); + params.put(SimpleWorkflowActionExecuter.PARAM_APPROVE_MOVE, true); + params.put(SimpleWorkflowActionExecuter.PARAM_REJECT_STEP, "rejectStep"); + params.put(SimpleWorkflowActionExecuter.PARAM_REJECT_FOLDER, this.rootNodeRef); + params.put(SimpleWorkflowActionExecuter.PARAM_REJECT_MOVE, false); + + Rule rule = createRule( + RuleType.INBOUND, + SimpleWorkflowActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + + assertTrue(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_SIMPLE_WORKFLOW)); + assertEquals("approveStep", this.nodeService.getProperty(newNodeRef, ContentModel.PROP_APPROVE_STEP)); + assertEquals(this.rootNodeRef, this.nodeService.getProperty(newNodeRef, ContentModel.PROP_APPROVE_FOLDER)); + assertTrue(((Boolean)this.nodeService.getProperty(newNodeRef, ContentModel.PROP_APPROVE_MOVE)).booleanValue()); + assertTrue(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_SIMPLE_WORKFLOW)); + assertEquals("rejectStep", this.nodeService.getProperty(newNodeRef, ContentModel.PROP_REJECT_STEP)); + assertEquals(this.rootNodeRef, this.nodeService.getProperty(newNodeRef, ContentModel.PROP_REJECT_FOLDER)); + assertFalse(((Boolean)this.nodeService.getProperty(newNodeRef, ContentModel.PROP_REJECT_MOVE)).booleanValue()); + + // System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + /** + * Test: + * rule type: inbound + * condition: in-category + * action: add-feature + */ + public void testInCategoryCondition() + { + // Create categories used in tests + createTestCategories(); + + try + { + Map params = new HashMap(1); + params.put(InCategoryEvaluator.PARAM_CATEGORY_ASPECT, this.regionCategorisationQName); + params.put(InCategoryEvaluator.PARAM_CATEGORY_VALUE, this.catROne); + + Map params2 = new HashMap(1); + params2.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params2, + InCategoryEvaluator.NAME, + params); + + this.ruleService.saveRule(this.nodeRef, rule); + + // Check rule does not get fired when a node without the aspect is added + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "noAspect"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef2); + assertFalse(this.nodeService.hasAspect(newNodeRef2, ContentModel.ASPECT_VERSIONABLE)); + + // Check rule gets fired when node contains category value + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "hasAspectAndValue"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + Map catProps = new HashMap(); + catProps.put(CAT_PROP_QNAME, this.catROne); + this.nodeService.addAspect(newNodeRef, this.regionCategorisationQName, catProps); + tx.commit(); + assertTrue(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // Check rule does not get fired when the node has the incorrect category value + UserTransaction tx3 = transactionService.getUserTransaction(); + tx3.begin(); + NodeRef newNodeRef3 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "hasAspectAndValue"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef3); + Map catProps3 = new HashMap(); + catProps3.put(CAT_PROP_QNAME, this.catRTwo); + this.nodeService.addAspect(newNodeRef3, this.regionCategorisationQName, catProps3); + tx3.commit(); + assertFalse(this.nodeService.hasAspect(newNodeRef3, ContentModel.ASPECT_VERSIONABLE)); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + catch (Exception exception) + { + throw new RuntimeException(exception); + } + } + + /** + * Test: + * rule type: inbound + * condition: no-condition + * action: link-category + */ + public void testLinkCategoryAction() + { + // Create categories used in tests + createTestCategories(); + + Map params = new HashMap(1); + params.put(LinkCategoryActionExecuter.PARAM_CATEGORY_ASPECT, this.regionCategorisationQName); + params.put(LinkCategoryActionExecuter.PARAM_CATEGORY_VALUE, this.catROne); + + Rule rule = createRule( + RuleType.INBOUND, + LinkCategoryActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "noAspect"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef2); + + // Check that the category value has been set + NodeRef setValue = (NodeRef)this.nodeService.getProperty(newNodeRef2, CAT_PROP_QNAME); + assertNotNull(setValue); + assertEquals(this.catROne, setValue); +} + + + /** + * Test: + * rule type: inbound + * condition: no-condition + * action: mail + * + * Note: this test will be removed from the standard list since it is not currently automated + */ + public void xtestMailAction() + { + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, null); + + Map params = new HashMap(1); + params.put(MailActionExecuter.PARAM_TO, "alfresco.test@gmail.com"); + params.put(MailActionExecuter.PARAM_SUBJECT, "Unit test"); + params.put(MailActionExecuter.PARAM_TEXT, "This is a test to check that the mail action is working."); + + Rule rule = createRule( + RuleType.INBOUND, + MailActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + + // An email should appear in the recipients email + + // System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + /** + * Test: + * rule type: inbound + * condition: no-condition() + * action: copy() + */ + public void testCopyAction() + { + Map params = new HashMap(1); + params.put(MoveActionExecuter.PARAM_DESTINATION_FOLDER, this.rootNodeRef); + params.put(MoveActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + params.put(MoveActionExecuter.PARAM_ASSOC_QNAME, QName.createQName(TEST_NAMESPACE, "copy")); + + Rule rule = createRule( + RuleType.INBOUND, + CopyActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "origional"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + + // Check that the created node is still there + List origRefs = this.nodeService.getChildAssocs( + this.nodeRef, + RegexQNamePattern.MATCH_ALL, + QName.createQName(TEST_NAMESPACE, "origional")); + assertNotNull(origRefs); + assertEquals(1, origRefs.size()); + NodeRef origNodeRef = origRefs.get(0).getChildRef(); + assertEquals(newNodeRef, origNodeRef); + + // Check that the created node has been copied + List copyChildAssocRefs = this.nodeService.getChildAssocs( + this.rootNodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "copy")); + assertNotNull(copyChildAssocRefs); + + // ********************************** + // NOTE: Changed expected result to get build running + // ********************************** + assertEquals(1, copyChildAssocRefs.size()); + + NodeRef copyNodeRef = copyChildAssocRefs.get(0).getChildRef(); + assertTrue(this.nodeService.hasAspect(copyNodeRef, ContentModel.ASPECT_COPIEDFROM)); + NodeRef source = (NodeRef)this.nodeService.getProperty(copyNodeRef, ContentModel.PROP_COPY_REFERENCE); + assertEquals(newNodeRef, source); + + // TODO test deep copy !! + } + + /** + * Test: + * rule type: inbound + * condition: no-condition() + * action: transform() + */ + public void testTransformAction() + { + if (this.transformerRegistry.getTransformer(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN) != null) + { + try + { + Map params = new HashMap(1); + params.put(TransformActionExecuter.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_TEXT_PLAIN); + params.put(TransformActionExecuter.PARAM_DESTINATION_FOLDER, this.rootNodeRef); + params.put(TransformActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + params.put(TransformActionExecuter.PARAM_ASSOC_QNAME, QName.createQName(TEST_NAMESPACE, "transformed")); + + Rule rule = createRule( + RuleType.INBOUND, + TransformActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + + Map props =new HashMap(1); + props.put(ContentModel.PROP_NAME, "test.xls"); + + // Create the node at the root + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "origional"), + ContentModel.TYPE_CONTENT, + props).getChildRef(); + + // Set some content on the origional + ContentWriter contentWriter = this.contentService.getWriter(newNodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_EXCEL); + File testFile = AbstractContentTransformerTest.loadQuickTestFile("xls"); + contentWriter.putContent(testFile); + + tx.commit(); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + + // Check that the created node is still there + List origRefs = this.nodeService.getChildAssocs( + this.nodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "origional")); + assertNotNull(origRefs); + assertEquals(1, origRefs.size()); + NodeRef origNodeRef = origRefs.get(0).getChildRef(); + assertEquals(newNodeRef, origNodeRef); + + // Check that the created node has been copied + List copyChildAssocRefs = this.nodeService.getChildAssocs( + this.rootNodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "transformed")); + assertNotNull(copyChildAssocRefs); + assertEquals(1, copyChildAssocRefs.size()); + NodeRef copyNodeRef = copyChildAssocRefs.get(0).getChildRef(); + assertTrue(this.nodeService.hasAspect(copyNodeRef, ContentModel.ASPECT_COPIEDFROM)); + NodeRef source = (NodeRef)this.nodeService.getProperty(copyNodeRef, ContentModel.PROP_COPY_REFERENCE); + assertEquals(newNodeRef, source); + + // Check the transformed content + ContentData contentData = (ContentData) nodeService.getProperty(copyNodeRef, ContentModel.PROP_CONTENT); + assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, contentData.getMimetype()); + + } + catch (Exception exception) + { + throw new RuntimeException(exception); + } + } + } + + /** + * Test image transformation + * + */ + public void testImageTransformAction() + { + if (this.transformerRegistry.getTransformer(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_IMAGE_JPEG) != null) + { + try + { + Map params = new HashMap(1); + params.put(ImageTransformActionExecuter.PARAM_DESTINATION_FOLDER, this.rootNodeRef); + params.put(ImageTransformActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + params.put(TransformActionExecuter.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_JPEG); + params.put(ImageTransformActionExecuter.PARAM_ASSOC_QNAME, QName.createQName(TEST_NAMESPACE, "transformed")); + params.put(ImageTransformActionExecuter.PARAM_CONVERT_COMMAND, "-negate"); + + Rule rule = createRule( + RuleType.INBOUND, + ImageTransformActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + + Map props =new HashMap(1); + props.put(ContentModel.PROP_NAME, "test.gif"); + + // Create the node at the root + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "origional"), + ContentModel.TYPE_CONTENT, + props).getChildRef(); + + // Set some content on the origional + ContentWriter contentWriter = this.contentService.getWriter(newNodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_IMAGE_GIF); + File testFile = AbstractContentTransformerTest.loadQuickTestFile("gif"); + contentWriter.putContent(testFile); + + tx.commit(); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + + // Check that the created node is still there + List origRefs = this.nodeService.getChildAssocs( + this.nodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "origional")); + assertNotNull(origRefs); + assertEquals(1, origRefs.size()); + NodeRef origNodeRef = origRefs.get(0).getChildRef(); + assertEquals(newNodeRef, origNodeRef); + + // Check that the created node has been copied + List copyChildAssocRefs = this.nodeService.getChildAssocs( + this.rootNodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "transformed")); + assertNotNull(copyChildAssocRefs); + assertEquals(1, copyChildAssocRefs.size()); + NodeRef copyNodeRef = copyChildAssocRefs.get(0).getChildRef(); + assertTrue(this.nodeService.hasAspect(copyNodeRef, ContentModel.ASPECT_COPIEDFROM)); + NodeRef source = (NodeRef)this.nodeService.getProperty(copyNodeRef, ContentModel.PROP_COPY_REFERENCE); + assertEquals(newNodeRef, source); + } + catch (Exception exception) + { + throw new RuntimeException(exception); + } + } + } + + /** + * Test: + * rule type: inbound + * condition: no-condition() + * action: move() + */ + public void testMoveAction() + { + Map params = new HashMap(1); + params.put(MoveActionExecuter.PARAM_DESTINATION_FOLDER, this.rootNodeRef); + params.put(MoveActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + params.put(MoveActionExecuter.PARAM_ASSOC_QNAME, QName.createQName(TEST_NAMESPACE, "copy")); + + Rule rule = createRule( + RuleType.INBOUND, + MoveActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "origional"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + + // Check that the created node has been moved + List origRefs = this.nodeService.getChildAssocs( + this.nodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "origional")); + assertNotNull(origRefs); + assertEquals(0, origRefs.size()); + + // Check that the created node is in the new location + List copyChildAssocRefs = this.nodeService.getChildAssocs( + this.rootNodeRef, + RegexQNamePattern.MATCH_ALL, QName.createQName(TEST_NAMESPACE, "copy")); + assertNotNull(copyChildAssocRefs); + assertEquals(1, copyChildAssocRefs.size()); + NodeRef movedNodeRef = copyChildAssocRefs.get(0).getChildRef(); + assertEquals(newNodeRef, movedNodeRef); + } + + /** + * Test: + * rule type: inbound + * condition: no-condition() + * action: checkout() + */ + public void testCheckOutAction() + { + Rule rule = createRule( + RuleType.INBOUND, + CheckOutActionExecuter.NAME, + null, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + NodeRef newNodeRef = null; + UserTransaction tx = this.transactionService.getUserTransaction(); + try + { + tx.begin(); + + // Create a new node + newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "checkout"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + + tx.commit(); + } + catch (Exception exception) + { + throw new RuntimeException(exception); + } + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + + // Check that the new node has been checked out + List children = this.nodeService.getChildAssocs(this.nodeRef); + assertNotNull(children); + assertEquals(3, children.size()); // includes rule folder + for (ChildAssociationRef child : children) + { + NodeRef childNodeRef = child.getChildRef(); + if (childNodeRef.equals(newNodeRef) == true) + { + // check that the node has been locked + LockStatus lockStatus = this.lockService.getLockStatus(childNodeRef); + assertEquals(LockStatus.LOCK_OWNER, lockStatus); + } + else if (this.nodeService.hasAspect(childNodeRef, ContentModel.ASPECT_WORKING_COPY) == true) + { + // assert that it is the working copy that relates to the origional node + NodeRef copiedFromNodeRef = (NodeRef)this.nodeService.getProperty(childNodeRef, ContentModel.PROP_COPY_REFERENCE); + assertEquals(newNodeRef, copiedFromNodeRef); + } + } + } + + /** + * Test: + * rule type: inbound + * condition: no-condition() + * action: checkin() + */ + @SuppressWarnings("unchecked") + public void testCheckInAction() + { + Map params = new HashMap(1); + params.put(CheckInActionExecuter.PARAM_DESCRIPTION, "The version description."); + + Rule rule = createRule( + RuleType.INBOUND, + CheckInActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + List list = TransactionUtil.executeInUserTransaction( + this.transactionService, + new TransactionUtil.TransactionWork>() + { + public List doWork() + { + // Create a new node and check-it out + NodeRef newNodeRef = RuleServiceCoverageTest.this.nodeService.createNode( + RuleServiceCoverageTest.this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "origional"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + NodeRef workingCopy = RuleServiceCoverageTest.this.cociService.checkout(newNodeRef); + + // Move the working copy into the actionable folder + RuleServiceCoverageTest.this.nodeService.moveNode( + workingCopy, + RuleServiceCoverageTest.this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "moved")); + + List result = new ArrayList(); + result.add(newNodeRef); + result.add(workingCopy); + return result; + } + + }); + + // Check that the working copy has been removed + assertFalse(this.nodeService.exists(list.get(1))); + + // Check that the origional is no longer locked + assertEquals(LockStatus.NO_LOCK, this.lockService.getLockStatus(list.get(0))); + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + } + + /** + * Check that the rules can be enabled and disabled + */ + public void testRulesDisabled() + { + Map actionParams = new HashMap(1); + actionParams.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + actionParams, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + this.ruleService.disableRules(this.nodeRef); + + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef); + assertFalse(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + this.ruleService.enableRules(this.nodeRef); + + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef2); + assertTrue(this.nodeService.hasAspect(newNodeRef2, ContentModel.ASPECT_VERSIONABLE)); + } + + /** + * Adds content to a given node. + *

    + * Used to trigger rules of type of incomming. + * + * @param nodeRef the node reference + */ + private void addContentToNode(NodeRef nodeRef) + { + ContentWriter contentWriter = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + assertNotNull(contentWriter); + contentWriter.putContent(STANDARD_TEXT_CONTENT + System.currentTimeMillis()); + } + + /** + * Test checkMandatoryProperties method + */ + public void testCheckMandatoryProperties() + { + Map actionParams = new HashMap(1); + actionParams.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Map condParams = new HashMap(1); + // should be setting the condition parameter here + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + actionParams, + ComparePropertyValueEvaluator.NAME, + condParams); + + this.ruleService.saveRule(this.nodeRef, rule); + + try + { + // Try and create a node .. should fail since the rule is invalid + Map props2 = getContentProperties(); + props2.put(ContentModel.PROP_NAME, "bobbins.doc"); + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + props2).getChildRef(); + addContentToNode(newNodeRef2); + fail("An exception should have been thrown since a mandatory parameter was missing from the condition."); + } + catch (Throwable ruleServiceException) + { + // Success since we where expecting the exception + } + } + + /** + * Test: + * rule type: inbound + * condition: match-text( + * text = .doc, + * operation = CONTAINS) + * action: add-features( + * aspect-name = versionable) + */ + public void testContainsTextCondition() + { + Map actionParams = new HashMap(1); + actionParams.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + // ActionCondition parameter's + Map condParams = new HashMap(1); + condParams.put(ComparePropertyValueEvaluator.PARAM_VALUE, ".doc"); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + actionParams, + ComparePropertyValueEvaluator.NAME, + condParams); + + this.ruleService.saveRule(this.nodeRef, rule); + + // Test condition failure + Map props1 = new HashMap(); + props1.put(ContentModel.PROP_NAME, "bobbins.txt"); + props1.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + props1).getChildRef(); + addContentToNode(newNodeRef); + + //Map map = this.nodeService.getProperties(newNodeRef); + //String value = (String)this.nodeService.getProperty(newNodeRef, ContentModel.PROP_NAME); + + assertFalse(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // Test condition success + Map props2 = new HashMap(); + props2.put(ContentModel.PROP_NAME, "bobbins.doc"); + props2.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + props2).getChildRef(); + addContentToNode(newNodeRef2); + assertTrue(this.nodeService.hasAspect( + newNodeRef2, + ContentModel.ASPECT_VERSIONABLE)); + + try + { + // Test name not set + NodeRef newNodeRef3 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + getContentProperties()).getChildRef(); + addContentToNode(newNodeRef3); + } + catch (RuleServiceException exception) + { + // Correct since text-match is a mandatory property + } + + // Test begins with + Map condParamsBegins = new HashMap(1); + condParamsBegins.put(ComparePropertyValueEvaluator.PARAM_VALUE, "bob*"); + rule.removeAllActionConditions(); + ActionCondition condition1 = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME, condParamsBegins); + rule.addActionCondition(condition1); + this.ruleService.saveRule(this.nodeRef, rule); + Map propsx = new HashMap(); + propsx.put(ContentModel.PROP_NAME, "mybobbins.doc"); + propsx.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + NodeRef newNodeRefx = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + propsx).getChildRef(); + addContentToNode(newNodeRefx); + assertFalse(this.nodeService.hasAspect(newNodeRefx, ContentModel.ASPECT_VERSIONABLE)); + Map propsy = new HashMap(); + propsy.put(ContentModel.PROP_NAME, "bobbins.doc"); + propsy.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + NodeRef newNodeRefy = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + propsy).getChildRef(); + addContentToNode(newNodeRefy); + assertTrue(this.nodeService.hasAspect( + newNodeRefy, + ContentModel.ASPECT_VERSIONABLE)); + + // Test ends with + Map condParamsEnds = new HashMap(1); + condParamsEnds.put(ComparePropertyValueEvaluator.PARAM_VALUE, "*s.doc"); + rule.removeAllActionConditions(); + ActionCondition condition2 = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME, condParamsEnds); + rule.addActionCondition(condition2); + this.ruleService.saveRule(this.nodeRef, rule); + Map propsa = new HashMap(); + propsa.put(ContentModel.PROP_NAME, "bobbins.document"); + propsa.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + NodeRef newNodeRefa = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + propsa).getChildRef(); + addContentToNode(newNodeRefa); + assertFalse(this.nodeService.hasAspect(newNodeRefa, ContentModel.ASPECT_VERSIONABLE)); + Map propsb = new HashMap(); + propsb.put(ContentModel.PROP_NAME, "bobbins.doc"); + propsb.put(ContentModel.PROP_CONTENT, CONTENT_DATA_TEXT); + NodeRef newNodeRefb = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTENT, + propsb).getChildRef(); + addContentToNode(newNodeRefb); + assertTrue(this.nodeService.hasAspect( + newNodeRefb, + ContentModel.ASPECT_VERSIONABLE)); + } + + /** + * Test: + * rule type: outbound + * condition: no-condition() + * action: add-features( + * aspect-name = versionable) + */ + public void testOutboundRuleType() + { + this.nodeService.addAspect(this.nodeRef, ContentModel.ASPECT_LOCKABLE, null); + + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + "outbound", + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + // Create a node + NodeRef newNodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTAINER).getChildRef(); + assertFalse(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // Move the node out of the actionable folder + this.nodeService.moveNode( + newNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children")); + assertTrue(this.nodeService.hasAspect(newNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + // Check the deletion of a node + + //System.out.println(NodeStoreInspector.dumpNodeStore(this.nodeService, this.testStoreRef)); + NodeRef newNodeRef2 = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTAINER).getChildRef(); + this.nodeService.deleteNode(newNodeRef2); + } + + /** + * Performance guideline test + * + */ + public void xtestPerformanceOfRuleExecution() + { + try + { + StopWatch sw = new StopWatch(); + + // Create actionable nodes + sw.start("create nodes with no rule executed"); + UserTransaction userTransaction1 = this.transactionService.getUserTransaction(); + userTransaction1.begin(); + + for (int i = 0; i < 100; i++) + { + this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CONTAINS, + ContentModel.ASSOC_CONTAINS, + ContentModel.TYPE_CONTAINER).getChildRef(); + assertFalse(this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE)); + } + + userTransaction1.commit(); + sw.stop(); + + Map params = new HashMap(1); + params.put("aspect-name", ContentModel.ASPECT_VERSIONABLE); + + Rule rule = createRule( + RuleType.INBOUND, + AddFeaturesActionExecuter.NAME, + params, + NoConditionEvaluator.NAME, + null); + + this.ruleService.saveRule(this.nodeRef, rule); + + sw.start("create nodes with one rule run (apply versionable aspect)"); + UserTransaction userTransaction2 = this.transactionService.getUserTransaction(); + userTransaction2.begin(); + + NodeRef[] nodeRefs = new NodeRef[100]; + for (int i = 0; i < 100; i++) + { + NodeRef nodeRef = this.nodeService.createNode( + this.nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, "children"), + ContentModel.TYPE_CONTAINER).getChildRef(); + addContentToNode(nodeRef); + nodeRefs[i] = nodeRef; + + // Check that the versionable aspect has not yet been applied + assertFalse(this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE)); + } + + userTransaction2.commit(); + sw.stop(); + + // Check that the versionable aspect has been applied to all the created nodes + for (NodeRef ref : nodeRefs) + { + assertTrue(this.nodeService.hasAspect(ref, ContentModel.ASPECT_VERSIONABLE)); + } + + System.out.println(sw.prettyPrint()); + } + catch (Exception exception) + { + throw new RuntimeException(exception); + } + } +} diff --git a/source/java/org/alfresco/repo/rule/RuleServiceImpl.java b/source/java/org/alfresco/repo/rule/RuleServiceImpl.java new file mode 100644 index 0000000000..802bc04cda --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleServiceImpl.java @@ -0,0 +1,956 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionModel; +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Rule service implementation. + *

    + * This service automatically binds to the transaction flush hooks. It will + * therefore participate in any flushes that occur during the transaction as + * well. + * + * @author Roy Wetherall + */ +public class RuleServiceImpl implements RuleService, RuntimeRuleService +{ + /** key against which to store rules pending on the current transaction */ + private static final String KEY_RULES_PENDING = "RuleServiceImpl.PendingRules"; + + /** key against which to store executed rules on the current transaction */ + private static final String KEY_RULES_EXECUTED = "RuleServiceImpl.ExecutedRules"; + + /** qname of assoc to rules */ + private QName ASSOC_NAME_RULES = QName.createQName(RuleModel.RULE_MODEL_URI, "rules"); + + /** + * The logger + */ + private static Log logger = LogFactory.getLog(RuleServiceImpl.class); + + /** + * The permission-safe node service + */ + private NodeService nodeService; + + /** + * The runtime node service (ignores permissions) + */ + private NodeService runtimeNodeService; + + /** + * The action service + */ + private ActionService actionService; + + /** + * The search service + */ + private SearchService searchService; + + /** + * The dictionary service + */ + private DictionaryService dictionaryService; + + /** + * The action service implementation which we need for some things. + */ + RuntimeActionService runtimeActionService; + + /** + * The rule cahce (set by default to an inactive rule cache) + */ + private RuleCache ruleCache = new InactiveRuleCache(); + + /** + * List of disabled node refs. The rules associated with these nodes will node be added to the pending list, and + * therefore not fired. This list is transient. + */ + private Set disabledNodeRefs = new HashSet(5); + + /** + * List of disabled rules. Any rules that appear in this list will not be added to the pending list and therefore + * not fired. + */ + private Set disabledRules = new HashSet(5); + + /** + * All the rule type currently registered + */ + private Map ruleTypes = new HashMap(); + + /** + * The rule transaction listener + */ + private TransactionListener ruleTransactionListener = new RuleTransactionListener(this); + + /** + * Set the permission-safe node service + * + * @param nodeService the permission-safe node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the direct node service + * + * @param nodeService the node service + */ + public void setRuntimeNodeService(NodeService runtimeNodeService) + { + this.runtimeNodeService = runtimeNodeService; + } + + /** + * Set the action service + * + * @param actionService the action service + */ + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set the runtime action service + * + * @param actionRegistration the action service + */ + public void setRuntimeActionService(RuntimeActionService runtimeActionService) + { + this.runtimeActionService = runtimeActionService; + } + + /** + * Set the search service + * + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Set the rule cache + * + * @param ruleCache the rule cache + */ + public void setRuleCache(RuleCache ruleCache) + { + this.ruleCache = ruleCache; + } + + /** + * Set the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Gets the saved rule folder reference + * + * @param nodeRef the node reference + * @return the node reference + */ + private NodeRef getSavedRuleFolderRef(NodeRef nodeRef) + { + NodeRef result = null; + + List assocs = this.runtimeNodeService.getChildAssocs( + nodeRef, + RegexQNamePattern.MATCH_ALL, + RuleModel.ASSOC_RULE_FOLDER); + if (assocs.size() > 1) + { + throw new ActionServiceException("There is more than one rule folder, which is invalid."); + } + else if (assocs.size() == 1) + { + result = assocs.get(0).getChildRef(); + } + + return result; + } + + /** + * @see org.alfresco.repo.rule.RuleService#getRuleTypes() + */ + public List getRuleTypes() + { + return new ArrayList(this.ruleTypes.values()); + } + + /** + * @see org.alfresco.repo.rule.RuleService#getRuleType(java.lang.String) + */ + public RuleType getRuleType(String name) + { + return this.ruleTypes.get(name); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleService#rulesEnabled(NodeRef) + */ + public boolean rulesEnabled(NodeRef nodeRef) + { + return (this.disabledNodeRefs.contains(nodeRef) == false); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleService#disableRules(NodeRef) + */ + public void disableRules(NodeRef nodeRef) + { + // Add the node to the set of disabled nodes + this.disabledNodeRefs.add(nodeRef); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleService#enableRules(NodeRef) + */ + public void enableRules(NodeRef nodeRef) + { + // Remove the node from the set of disabled nodes + this.disabledNodeRefs.remove(nodeRef); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleService#disableRule(org.alfresco.service.cmr.rule.Rule) + */ + public void disableRule(Rule rule) + { + this.disabledRules.add(rule); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleService#enableRule(org.alfresco.service.cmr.rule.Rule) + */ + public void enableRule(Rule rule) + { + this.disabledRules.remove(rule); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleService#hasRules(org.alfresco.repo.ref.NodeRef) + */ + public boolean hasRules(NodeRef nodeRef) + { + return getRules(nodeRef).size() != 0; + } + + /** + * @see org.alfresco.repo.rule.RuleService#getRules(org.alfresco.repo.ref.NodeRef) + */ + public List getRules(NodeRef nodeRef) + { + return getRules(nodeRef, true, null); + } + + /** + * @see org.alfresco.repo.rule.RuleService#getRules(org.alfresco.repo.ref.NodeRef, boolean) + */ + public List getRules(NodeRef nodeRef, boolean includeInherited) + { + return getRules(nodeRef, includeInherited, null); + } + + /** + * @see org.alfresco.repo.rule.RuleService#getRulesByRuleType(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.rule.RuleType) + */ + public List getRules(NodeRef nodeRef, boolean includeInherited, String ruleTypeName) + { + List rules = new ArrayList(); + + if (this.runtimeNodeService.exists(nodeRef) == true && checkNodeType(nodeRef) == true) + { + if (includeInherited == true) + { + // Get any inherited rules + for (Rule rule : getInheritedRules(nodeRef, ruleTypeName, null)) + { + // Ensure rules are not duplicated in the list + if (rules.contains(rule) == false) + { + rules.add(rule); + } + } + } + + if (this.runtimeNodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + { + NodeRef ruleFolder = getSavedRuleFolderRef(nodeRef); + if (ruleFolder != null) + { + List allRules = this.ruleCache.getRules(nodeRef); + if (allRules == null) + { + allRules = new ArrayList(); + + // Get the rules for this node + List ruleChildAssocRefs = + this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES); + for (ChildAssociationRef ruleChildAssocRef : ruleChildAssocRefs) + { + // Create the rule and add to the list + NodeRef ruleNodeRef = ruleChildAssocRef.getChildRef(); + Rule rule = createRule(nodeRef, ruleNodeRef); + allRules.add(rule); + } + + // Add the list to the cache + this.ruleCache.setRules(nodeRef, allRules); + } + + // Build the list of rules that is returned to the client + for (Rule rule : allRules) + { + if ((rules.contains(rule) == false) && + (ruleTypeName == null || ruleTypeName.equals(rule.getRuleTypeName()) == true)) + { + rules.add(rule); + } + } + } + } + } + + return rules; + } + + /** + * Looks at the type of the node and indicates whether the node can have rules associated with it + * + * @param nodeRef the node reference + * @return true if the node can have rule associated with it (inherited or otherwise) + */ + private boolean checkNodeType(NodeRef nodeRef) + { + boolean result = true; + + QName nodeType = this.nodeService.getType(nodeRef); + if (this.dictionaryService.isSubClass(nodeType, ContentModel.TYPE_SYSTEM_FOLDER) == true || + this.dictionaryService.isSubClass(nodeType, ActionModel.TYPE_ACTION) == true || + this.dictionaryService.isSubClass(nodeType, ActionModel.TYPE_ACTION_CONDITION) == true || + this.dictionaryService.isSubClass(nodeType, ActionModel.TYPE_ACTION_PARAMETER) == true) + { + result = false; + + if (logger.isDebugEnabled() == true) + { + logger.debug("A node of type " + nodeType.toString() + " was checked and can not have rules."); + } + } + + return result; + } + + /** + * Gets the inherited rules for a given node reference + * + * @param nodeRef the nodeRef + * @param ruleTypeName the rule type (null if all applicable) + * @return a list of inherited rules (empty if none) + */ + private List getInheritedRules(NodeRef nodeRef, String ruleTypeName, Set visitedNodeRefs) + { + List inheritedRules = new ArrayList(); + + // Create the visited nodes set if it has not already been created + if (visitedNodeRefs == null) + { + visitedNodeRefs = new HashSet(); + } + + // This check prevents stack over flow when we have a cyclic node graph + if (visitedNodeRefs.contains(nodeRef) == false) + { + visitedNodeRefs.add(nodeRef); + + List allInheritedRules = this.ruleCache.getInheritedRules(nodeRef); + if (allInheritedRules == null) + { + allInheritedRules = new ArrayList(); + List parents = this.runtimeNodeService.getParentAssocs(nodeRef); + for (ChildAssociationRef parent : parents) + { + List rules = getRules(parent.getParentRef(), false); + for (Rule rule : rules) + { + // Add is we hanvn't already added and it should be applied to the children + if (rule.isAppliedToChildren() == true && allInheritedRules.contains(rule) == false) + { + allInheritedRules.add(rule); + } + } + + for (Rule rule : getInheritedRules(parent.getParentRef(), ruleTypeName, visitedNodeRefs)) + { + // Ensure that we don't get any rule duplication (don't use a set cos we want to preserve order) + if (allInheritedRules.contains(rule) == false) + { + allInheritedRules.add(rule); + } + } + } + + // Add the list of inherited rules to the cache + this.ruleCache.setInheritedRules(nodeRef, allInheritedRules); + } + + if (ruleTypeName == null) + { + inheritedRules = allInheritedRules; + } + else + { + // Filter the rule list by rule type + for (Rule rule : allInheritedRules) + { + if (rule.getRuleTypeName().equals(ruleTypeName) == true) + { + inheritedRules.add(rule); + } + } + } + } + + return inheritedRules; + } + + /** + * @see org.alfresco.repo.rule.RuleService#getRule(String) + */ + public Rule getRule(NodeRef nodeRef, String ruleId) + { + Rule rule = null; + + if (this.runtimeNodeService.exists(nodeRef) == true) + { + NodeRef ruleNodeRef = getRuleNodeRefFromId(nodeRef, ruleId); + if (ruleNodeRef != null) + { + rule = createRule(nodeRef, ruleNodeRef); + } + } + + return rule; + } + + /** + * Gets the rule node ref from the action id + * + * @param nodeRef the node reference + * @param actionId the rule id + * @return the rule node reference + */ + private NodeRef getRuleNodeRefFromId(NodeRef nodeRef, String ruleId) + { + NodeRef result = null; + if (this.runtimeNodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + { + NodeRef ruleFolder = getSavedRuleFolderRef(nodeRef); + if (ruleFolder != null) + { + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(); + namespacePrefixResolver.registerNamespace(NamespaceService.SYSTEM_MODEL_PREFIX, NamespaceService.SYSTEM_MODEL_1_0_URI); + + List nodeRefs = searchService.selectNodes( + ruleFolder, + "*[@sys:" + ContentModel.PROP_NODE_UUID.getLocalName() + "='" + ruleId + "']", + null, + namespacePrefixResolver, + false); + if (nodeRefs.size() != 0) + { + result = nodeRefs.get(0); + } + } + } + + return result; + } + + /** + * Create the rule object from the rule node reference + * + * @param ruleNodeRef the rule node reference + * @return the rule + */ + private Rule createRule(NodeRef owningNodeRef, NodeRef ruleNodeRef) + { + // Get the rule properties + Map props = this.nodeService.getProperties(ruleNodeRef); + + // Create the rule + String ruleTypeName = (String)props.get(RuleModel.PROP_RULE_TYPE); + Rule rule = new RuleImpl(ruleNodeRef.getId(), ruleTypeName, owningNodeRef); + + // Set the other rule properties + boolean isAppliedToChildren = false; + Boolean value = (Boolean)props.get(RuleModel.PROP_APPLY_TO_CHILDREN); + if (value != null) + { + isAppliedToChildren = value.booleanValue(); + } + rule.applyToChildren(isAppliedToChildren); + + // Populate the composite action details + runtimeActionService.populateCompositeAction(ruleNodeRef, rule); + + return rule; + } + + /** + * @see org.alfresco.repo.rule.RuleService#createRule(org.alfresco.repo.rule.RuleType) + */ + public Rule createRule(String ruleTypeName) + { + // Create the new rule, giving it a unique rule id + String id = GUID.generate(); + return new RuleImpl(id, ruleTypeName, null); + } + + /** + * @see org.alfresco.repo.rule.RuleService#saveRule(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.rule.Rule) + */ + public void saveRule(NodeRef nodeRef, Rule rule) + { + if (this.nodeService.exists(nodeRef) == false) + { + throw new RuleServiceException("The node does not exist."); + } + + NodeRef ruleNodeRef = getRuleNodeRefFromId(nodeRef, rule.getId()); + if (ruleNodeRef == null) + { + if (this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == false) + { + // Add the actionable aspect + this.nodeService.addAspect(nodeRef, RuleModel.ASPECT_RULES, null); + } + + Map props = new HashMap(3); + props.put(RuleModel.PROP_RULE_TYPE, rule.getRuleTypeName()); + props.put(ActionModel.PROP_DEFINITION_NAME, rule.getActionDefinitionName()); + props.put(ContentModel.PROP_NODE_UUID, rule.getId()); + + // Create the action node + ruleNodeRef = this.nodeService.createNode( + getSavedRuleFolderRef(nodeRef), + ContentModel.ASSOC_CONTAINS, + ASSOC_NAME_RULES, + RuleModel.TYPE_RULE, + props).getChildRef(); + + // Update the created details + ((RuleImpl)rule).setCreator((String)this.nodeService.getProperty(ruleNodeRef, ContentModel.PROP_CREATOR)); + ((RuleImpl)rule).setCreatedDate((Date)this.nodeService.getProperty(ruleNodeRef, ContentModel.PROP_CREATED)); + } + + // Update the properties of the rule + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_APPLY_TO_CHILDREN, rule.isAppliedToChildren()); + + // Save the remainder of the rule as a composite action + runtimeActionService.saveActionImpl(nodeRef, ruleNodeRef, rule); + } + + /** + * @see org.alfresco.repo.rule.RuleService#removeRule(org.alfresco.repo.ref.NodeRef, org.alfresco.repo.rule.RuleImpl) + */ + public void removeRule(NodeRef nodeRef, Rule rule) + { + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + { + disableRules(nodeRef); + try + { + NodeRef ruleNodeRef = getRuleNodeRefFromId(nodeRef, rule.getId()); + if (ruleNodeRef != null) + { + this.nodeService.removeChild(getSavedRuleFolderRef(nodeRef), ruleNodeRef); + } + } + finally + { + enableRules(nodeRef); + } + } + } + + /** + * @see org.alfresco.repo.rule.RuleService#removeAllRules(NodeRef) + */ + public void removeAllRules(NodeRef nodeRef) + { + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + { + NodeRef folder = getSavedRuleFolderRef(nodeRef); + if (folder != null) + { + List ruleChildAssocs = this.nodeService.getChildAssocs( + folder, + RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES); + for (ChildAssociationRef ruleChildAssoc : ruleChildAssocs) + { + this.nodeService.removeChild(folder, ruleChildAssoc.getChildRef()); + } + } + } + } + + @SuppressWarnings("unchecked") + public void addRulePendingExecution(NodeRef actionableNodeRef, NodeRef actionedUponNodeRef, Rule rule) + { + addRulePendingExecution(actionableNodeRef, actionedUponNodeRef, rule, false); + } + + @SuppressWarnings("unchecked") + public void addRulePendingExecution(NodeRef actionableNodeRef, NodeRef actionedUponNodeRef, Rule rule, boolean executeAtEnd) + { + // First check to see if the node has been disabled + if (this.disabledNodeRefs.contains(rule.getOwningNodeRef()) == false && + this.disabledRules.contains(rule) == false) + { + PendingRuleData pendingRuleData = new PendingRuleData(actionableNodeRef, actionedUponNodeRef, rule, executeAtEnd); + Set executedRules = + (Set) AlfrescoTransactionSupport.getResource(KEY_RULES_EXECUTED); + + if (executedRules == null || executedRules.contains(new ExecutedRuleData(actionableNodeRef, rule)) == false) + { + Set pendingRules = + (Set) AlfrescoTransactionSupport.getResource(KEY_RULES_PENDING); + if (pendingRules == null) + { + // bind pending rules to the current transaction + pendingRules = new HashSet(); + AlfrescoTransactionSupport.bindResource(KEY_RULES_PENDING, pendingRules); + // bind the rule transaction listener + AlfrescoTransactionSupport.bindListener(this.ruleTransactionListener); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Rule '" + rule.getTitle() + "' has been added pending execution to action upon node '" + actionedUponNodeRef.getId() + "'"); + } + } + + // Prevent hte same rule being executed more than one in the same transaction + pendingRules.add(pendingRuleData); + } + } + else + { + if (logger.isDebugEnabled() == true) + { + logger.debug("The rule '" + rule.getTitle() + "' or the node '" + rule.getOwningNodeRef().getId() + "' has been disabled."); + } + } + } + + /** + * @see org.alfresco.repo.rule.RuleService#executePendingRules() + */ + public void executePendingRules() + { + AlfrescoTransactionSupport.bindResource(KEY_RULES_EXECUTED, new HashSet()); + try + { + List executeAtEndRules = new ArrayList(); + executePendingRulesImpl(executeAtEndRules); + for (PendingRuleData data : executeAtEndRules) + { + executePendingRule(data); + } + } + finally + { + AlfrescoTransactionSupport.unbindResource(KEY_RULES_EXECUTED); + } + } + + /** + * Executes the pending rules, iterating until all pending rules have been executed + */ + @SuppressWarnings("unchecked") + private void executePendingRulesImpl(List executeAtEndRules) + { + // get the transaction-local rules to execute + Set pendingRules = + (Set) AlfrescoTransactionSupport.getResource(KEY_RULES_PENDING); + // only execute if there are rules present + if (pendingRules != null && !pendingRules.isEmpty()) + { + PendingRuleData[] pendingRulesArr = pendingRules.toArray(new PendingRuleData[0]); + // remove all pending rules from the transaction + AlfrescoTransactionSupport.unbindResource(KEY_RULES_PENDING); + // execute each rule + for (PendingRuleData pendingRule : pendingRulesArr) + { + if (pendingRule.getExecuteAtEnd() == false) + { + executePendingRule(pendingRule); + } + else + { + executeAtEndRules.add(pendingRule); + } + } + + // Run any rules that have been marked as pending during execution + executePendingRulesImpl(executeAtEndRules); + } + } + + /** + * Executes a pending rule + * + * @param pendingRule the pending rule data object + */ + @SuppressWarnings("unchecked") + private void executePendingRule(PendingRuleData pendingRule) + { + NodeRef actionableNodeRef = pendingRule.getActionableNodeRef(); + NodeRef actionedUponNodeRef = pendingRule.getActionedUponNodeRef(); + Rule rule = pendingRule.getRule(); + + // Evaluate the condition + if (this.actionService.evaluateAction(rule, actionedUponNodeRef) == true) + { + // Add the rule to the executed rule list + // (do this before this is executed to prevent rules being added to the pending list) + Set executedRules = + (Set) AlfrescoTransactionSupport.getResource(KEY_RULES_EXECUTED); + executedRules.add(new ExecutedRuleData(actionableNodeRef, rule)); + + // Execute the rule + this.actionService.executeAction(rule, actionedUponNodeRef); + } + } + + /** + * Register the rule type + * + * @param ruleTypeAdapter the rule type adapter + */ + public void registerRuleType(RuleType ruleType) + { + this.ruleTypes.put(ruleType.getName(), ruleType); + } + + /** + * Helper class to contain the information about a rule that is executed + * + * @author Roy Wetherall + */ + private class ExecutedRuleData + { + + protected NodeRef actionableNodeRef; + protected Rule rule; + + public ExecutedRuleData(NodeRef actionableNodeRef, Rule rule) + { + this.actionableNodeRef = actionableNodeRef; + this.rule = rule; + } + + public NodeRef getActionableNodeRef() + { + return actionableNodeRef; + } + + public Rule getRule() + { + return rule; + } + + @Override + public int hashCode() + { + int i = actionableNodeRef.hashCode(); + i = (i*37) + rule.hashCode(); + return i; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof ExecutedRuleData) + { + ExecutedRuleData that = (ExecutedRuleData) obj; + return (this.actionableNodeRef.equals(that.actionableNodeRef) && + this.rule.equals(that.rule)); + } + else + { + return false; + } + } + } + + /** + * Helper class to contain the information about a rule that is pending execution + * + * @author Roy Wetherall + */ + private class PendingRuleData extends ExecutedRuleData + { + private NodeRef actionedUponNodeRef; + private boolean executeAtEnd = false; + + public PendingRuleData(NodeRef actionableNodeRef, NodeRef actionedUponNodeRef, Rule rule, boolean executeAtEnd) + { + super(actionableNodeRef, rule); + this.actionedUponNodeRef = actionedUponNodeRef; + this.executeAtEnd = executeAtEnd; + } + + public NodeRef getActionedUponNodeRef() + { + return actionedUponNodeRef; + } + + public boolean getExecuteAtEnd() + { + return this.executeAtEnd; + } + + @Override + public int hashCode() + { + int i = super.hashCode(); + i = (i*37) + actionedUponNodeRef.hashCode(); + return i; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof PendingRuleData) + { + PendingRuleData that = (PendingRuleData) obj; + return (this.actionableNodeRef.equals(that.actionableNodeRef) && + this.actionedUponNodeRef.equals(that.actionedUponNodeRef) && + this.rule.equals(that.rule)); + } + else + { + return false; + } + } + } + + /** + * Inactive rule cache + * + * @author Roy Wetherall + */ + private class InactiveRuleCache implements RuleCache + { + /** + * @see org.alfresco.repo.rule.RuleCache#getRules(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getRules(NodeRef nodeRef) + { + // do nothing + return null; + } + + /** + * @see org.alfresco.repo.rule.RuleCache#setRules(org.alfresco.service.cmr.repository.NodeRef, List) + */ + public void setRules(NodeRef nodeRef, List rules) + { + // do nothing + } + + /** + * @see org.alfresco.repo.rule.RuleCache#dirtyRules(org.alfresco.service.cmr.repository.NodeRef) + */ + public void dirtyRules(NodeRef nodeRef) + { + // do nothing + } + + /** + * @see org.alfresco.repo.rule.RuleCache#getInheritedRules(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getInheritedRules(NodeRef nodeRef) + { + // do nothing + return null; + } + + /** + * @see org.alfresco.repo.rule.RuleCache#setInheritedRules(org.alfresco.service.cmr.repository.NodeRef, List) + */ + public void setInheritedRules(NodeRef nodeRef, List rules) + { + // do nothing + } + } +} diff --git a/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java b/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java new file mode 100644 index 0000000000..f3216b9d8c --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.io.File; +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.executer.ImageTransformActionExecuter; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.CyclicChildRelationshipException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.namespace.QName; + + +/** + * Rule service implementation test + * + * @author Roy Wetherall + */ +public class RuleServiceImplTest extends BaseRuleTest +{ + + /** + * Test get rule type + */ + public void testGetRuleType() + { + List ruleTypes = this.ruleService.getRuleTypes(); + assertNotNull(ruleTypes); + + // Visual check to make sure that the display labels are being returned correctly + for (RuleType type : ruleTypes) + { + System.out.println(type.getDisplayLabel()); + } + } + + /** + * Test createRule + */ + public void testCreateRule() + { + Rule newRule = this.ruleService.createRule("ruleType1"); + assertNotNull(newRule); + assertNotNull(newRule.getId()); + assertEquals("ruleType1", newRule.getRuleTypeName()); + } + + /** + * Test addRule + * + */ + public void testAddRule() + { + Rule newRule = createTestRule(); + String ruleId = newRule.getId(); + this.ruleService.saveRule(this.nodeRef, newRule); + + Rule savedRule = this.ruleService.getRule(this.nodeRef, ruleId); + assertNotNull(savedRule); + assertFalse(savedRule.isAppliedToChildren()); + + savedRule.applyToChildren(true); + this.ruleService.saveRule(this.nodeRef, savedRule); + + Rule savedRule2 = this.ruleService.getRule(this.nodeRef, ruleId); + assertNotNull(savedRule2); + assertTrue(savedRule2.isAppliedToChildren()); + } + + public void testRemoveAllRules() + { + this.ruleService.removeAllRules(this.nodeRef); + List rules1 = this.ruleService.getRules(this.nodeRef); + assertNotNull(rules1); + assertEquals(0, rules1.size()); + + Rule newRule = this.ruleService.createRule(ruleType.getName()); + this.ruleService.saveRule(this.nodeRef, newRule); + Rule newRule2 = this.ruleService.createRule(ruleType.getName()); + this.ruleService.saveRule(this.nodeRef, newRule2); + + List rules2 = this.ruleService.getRules(this.nodeRef); + assertNotNull(rules2); + assertEquals(2, rules2.size()); + + this.ruleService.removeAllRules(this.nodeRef); + + List rules3 = this.ruleService.getRules(this.nodeRef); + assertNotNull(rules3); + assertEquals(0, rules3.size()); + + } + + /** + * Test get rules + */ + public void testGetRules() + { + // Check that there are no rules associationed with the node + List noRules = this.ruleService.getRules(this.nodeRef); + assertNotNull(noRules); + assertEquals(0, noRules.size()); + + // Check that we still get nothing back after the details of the node + // have been cached in the rule store + List noRulesAfterCache = this.ruleService.getRules(this.nodeRef); + assertNotNull(noRulesAfterCache); + assertEquals(0, noRulesAfterCache.size()); + + // Add a rule to the node + testAddRule(); + + // Get the rule from the rule service + List rules = this.ruleService.getRules(this.nodeRef); + assertNotNull(rules); + assertEquals(1, rules.size()); + + // Check the details of the rule + Rule rule = rules.get(0); + assertEquals("title", rule.getTitle()); + assertEquals("description", rule.getDescription()); + assertNotNull(rule.getCreatedDate()); + assertNotNull(rule.getModifiedDate()); + + // Check that the condition action have been retireved correctly + List conditions = rule.getActionConditions(); + assertNotNull(conditions); + assertEquals(1, conditions.size()); + List actions = rule.getActions(); + assertNotNull(actions); + assertEquals(1, actions.size()); + } + + /** + * Test disabling the rules + */ + public void testRulesDisabled() + { + testAddRule(); + assertTrue(this.ruleService.rulesEnabled(this.nodeRef)); + this.ruleService.disableRules(this.nodeRef); + assertFalse(this.ruleService.rulesEnabled(this.nodeRef)); + this.ruleService.enableRules(this.nodeRef); + assertTrue(this.ruleService.rulesEnabled(this.nodeRef)); + } + + /** + * Helper method to easily create a new node which can be actionable (or not) + * + * @param parent the parent node + * @param isActionable indicates whether the node is actionable or not + */ + private NodeRef createNewNode(NodeRef parent, boolean isActionable) + { + NodeRef newNodeRef = this.nodeService.createNode(parent, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTAINER).getChildRef(); + return newNodeRef; + } + + /** + * Tests the rule inheritance within the store, checking that the cache is reset correctly when + * rules are added and removed. + */ + public void testRuleInheritance() + { + // Create the nodes and rules + + NodeRef rootWithRules = createNewNode(this.rootNodeRef, true); + Rule rule1 = createTestRule(); + this.ruleService.saveRule(rootWithRules, rule1); + Rule rule2 = createTestRule(true); + this.ruleService.saveRule(rootWithRules, rule2); + + NodeRef nonActionableChild = createNewNode(rootWithRules, false); + + NodeRef childWithRules = createNewNode(nonActionableChild, true); + Rule rule3 = createTestRule(); + this.ruleService.saveRule(childWithRules, rule3); + Rule rule4 = createTestRule(true); + this.ruleService.saveRule(childWithRules, rule4); + + NodeRef rootWithRules2 = createNewNode(this.rootNodeRef, true); + this.nodeService.addChild( + rootWithRules2, + childWithRules, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode")); + Rule rule5 = createTestRule(); + this.ruleService.saveRule(rootWithRules2, rule5); + Rule rule6 = createTestRule(true); + this.ruleService.saveRule(rootWithRules2, rule6); + + // Check that the rules are inherited in the correct way + + List allRules = this.ruleService.getRules(childWithRules, true); + assertNotNull(allRules); + assertEquals(4, allRules.size()); + assertTrue(allRules.contains(rule2)); + assertTrue(allRules.contains(rule3)); + assertTrue(allRules.contains(rule4)); + assertTrue(allRules.contains(rule6)); + + // Check the owning node ref + int count = 0; + for (Rule rule : allRules) + { + if (rule.getOwningNodeRef() == childWithRules) + { + count++; + } + } + assertEquals(2, count); + + List myRules = this.ruleService.getRules(childWithRules, false); + assertNotNull(myRules); + assertEquals(2, myRules.size()); + assertTrue(myRules.contains(rule3)); + assertTrue(myRules.contains(rule4)); + + List allRules2 = this.ruleService.getRules(nonActionableChild, true); + assertNotNull(allRules2); + assertEquals(1, allRules2.size()); + assertTrue(allRules2.contains(rule2)); + + List myRules2 = this.ruleService.getRules(nonActionableChild, false); + assertNotNull(myRules2); + assertEquals(0, myRules2.size()); + + List allRules3 = this.ruleService.getRules(rootWithRules, true); + assertNotNull(allRules3); + assertEquals(2, allRules3.size()); + assertTrue(allRules3.contains(rule1)); + assertTrue(allRules3.contains(rule2)); + + List myRules3 = this.ruleService.getRules(rootWithRules, false); + assertNotNull(myRules3); + assertEquals(2, myRules3.size()); + assertTrue(myRules3.contains(rule1)); + assertTrue(myRules3.contains(rule2)); + + List allRules4 = this.ruleService.getRules(rootWithRules2, true); + assertNotNull(allRules4); + assertEquals(2, allRules4.size()); + assertTrue(allRules4.contains(rule5)); + assertTrue(allRules4.contains(rule6)); + + List myRules4 = this.ruleService.getRules(rootWithRules2, false); + assertNotNull(myRules4); + assertEquals(2, myRules4.size()); + assertTrue(myRules4.contains(rule5)); + assertTrue(myRules4.contains(rule6)); + + // Take the root node and add another rule + + Rule rule7 = createTestRule(true); + this.ruleService.saveRule(rootWithRules, rule7); + + List allRules5 = this.ruleService.getRules(childWithRules, true); + assertNotNull(allRules5); + assertEquals(5, allRules5.size()); + assertTrue(allRules5.contains(rule2)); + assertTrue(allRules5.contains(rule3)); + assertTrue(allRules5.contains(rule4)); + assertTrue(allRules5.contains(rule6)); + assertTrue(allRules5.contains(rule7)); + + List allRules6 = this.ruleService.getRules(nonActionableChild, true); + assertNotNull(allRules6); + assertEquals(2, allRules6.size()); + assertTrue(allRules6.contains(rule2)); + assertTrue(allRules6.contains(rule7)); + + List allRules7 = this.ruleService.getRules(rootWithRules, true); + assertNotNull(allRules7); + assertEquals(3, allRules7.size()); + assertTrue(allRules7.contains(rule1)); + assertTrue(allRules7.contains(rule2)); + assertTrue(allRules7.contains(rule7)); + + List allRules8 = this.ruleService.getRules(rootWithRules2, true); + assertNotNull(allRules8); + assertEquals(2, allRules8.size()); + assertTrue(allRules8.contains(rule5)); + assertTrue(allRules8.contains(rule6)); + + // Take the root node and and remove a rule + + this.ruleService.removeRule(rootWithRules, rule7); + + List allRules9 = this.ruleService.getRules(childWithRules, true); + assertNotNull(allRules9); + assertEquals(4, allRules9.size()); + assertTrue(allRules9.contains(rule2)); + assertTrue(allRules9.contains(rule3)); + assertTrue(allRules9.contains(rule4)); + assertTrue(allRules9.contains(rule6)); + + List allRules10 = this.ruleService.getRules(nonActionableChild, true); + assertNotNull(allRules10); + assertEquals(1, allRules10.size()); + assertTrue(allRules10.contains(rule2)); + + List allRules11 = this.ruleService.getRules(rootWithRules, true); + assertNotNull(allRules11); + assertEquals(2, allRules11.size()); + assertTrue(allRules11.contains(rule1)); + assertTrue(allRules11.contains(rule2)); + + List allRules12 = this.ruleService.getRules(rootWithRules2, true); + assertNotNull(allRules12); + assertEquals(2, allRules12.size()); + assertTrue(allRules12.contains(rule5)); + assertTrue(allRules12.contains(rule6)); + + // Delete an association + + this.nodeService.removeChild(rootWithRules2, childWithRules); + + List allRules13 = this.ruleService.getRules(childWithRules, true); + assertNotNull(allRules13); + assertEquals(3, allRules13.size()); + assertTrue(allRules13.contains(rule2)); + assertTrue(allRules13.contains(rule3)); + assertTrue(allRules13.contains(rule4)); + + List allRules14 = this.ruleService.getRules(nonActionableChild, true); + assertNotNull(allRules14); + assertEquals(1, allRules14.size()); + assertTrue(allRules14.contains(rule2)); + + List allRules15 = this.ruleService.getRules(rootWithRules, true); + assertNotNull(allRules15); + assertEquals(2, allRules15.size()); + assertTrue(allRules15.contains(rule1)); + assertTrue(allRules15.contains(rule2)); + + List allRules16 = this.ruleService.getRules(rootWithRules2, true); + assertNotNull(allRules16); + assertEquals(2, allRules16.size()); + assertTrue(allRules16.contains(rule5)); + assertTrue(allRules16.contains(rule6)); + + this.ruleService.disableRules(rootWithRules2); + try + { + // Add an association + this.nodeService.addChild( + rootWithRules2, + childWithRules, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode")); + } + finally + { + this.ruleService.enableRules(rootWithRules2); + } + + List allRules17 = this.ruleService.getRules(childWithRules, true); + assertNotNull(allRules17); + assertEquals(4, allRules17.size()); + assertTrue(allRules17.contains(rule2)); + assertTrue(allRules17.contains(rule3)); + assertTrue(allRules17.contains(rule4)); + assertTrue(allRules17.contains(rule6)); + + List allRules18 = this.ruleService.getRules(nonActionableChild, true); + assertNotNull(allRules18); + assertEquals(1, allRules18.size()); + assertTrue(allRules18.contains(rule2)); + + List allRules19 = this.ruleService.getRules(rootWithRules, true); + assertNotNull(allRules19); + assertEquals(2, allRules19.size()); + assertTrue(allRules19.contains(rule1)); + assertTrue(allRules19.contains(rule2)); + + List allRules20 = this.ruleService.getRules(rootWithRules2, true); + assertNotNull(allRules20); + assertEquals(2, allRules20.size()); + assertTrue(allRules20.contains(rule5)); + assertTrue(allRules20.contains(rule6)); + + // Delete node + + this.nodeService.deleteNode(rootWithRules2); + + List allRules21 = this.ruleService.getRules(childWithRules, true); + assertNotNull(allRules21); + assertEquals(3, allRules21.size()); + assertTrue(allRules21.contains(rule2)); + assertTrue(allRules21.contains(rule3)); + assertTrue(allRules21.contains(rule4)); + + List allRules22 = this.ruleService.getRules(nonActionableChild, true); + assertNotNull(allRules22); + assertEquals(1, allRules22.size()); + assertTrue(allRules22.contains(rule2)); + + List allRules23 = this.ruleService.getRules(rootWithRules, true); + assertNotNull(allRules23); + assertEquals(2, allRules23.size()); + assertTrue(allRules23.contains(rule1)); + assertTrue(allRules23.contains(rule2)); + } + + /** + * Ensure that the rule store can cope with a cyclic node graph + * + * @throws Exception + */ + public void testCyclicGraphWithInheritedRules() + throws Exception + { + NodeRef nodeRef1 = createNewNode(this.rootNodeRef, true); + NodeRef nodeRef2 = createNewNode(nodeRef1, true); + NodeRef nodeRef3 = createNewNode(nodeRef2, true); + try + { + this.nodeService.addChild(nodeRef3, nodeRef1, ContentModel.ASSOC_CHILDREN, QName.createQName("{test}loop")); + fail("Expected detection of cyclic relationship"); + } + catch (CyclicChildRelationshipException e) + { + // expected + // the node will still have been created in the current transaction, although the txn will be rollback-only + } + + Rule rule1 = createTestRule(true); + this.ruleService.saveRule(nodeRef1, rule1); + Rule rule2 = createTestRule(true); + this.ruleService.saveRule(nodeRef2, rule2); + Rule rule3 = createTestRule(true); + this.ruleService.saveRule(nodeRef3, rule3); + + List allRules1 = this.ruleService.getRules(nodeRef1, true); + assertNotNull(allRules1); + assertEquals(3, allRules1.size()); + assertTrue(allRules1.contains(rule1)); + assertTrue(allRules1.contains(rule2)); + assertTrue(allRules1.contains(rule3)); + + List allRules2 = this.ruleService.getRules(nodeRef2, true); + assertNotNull(allRules2); + assertEquals(3, allRules2.size()); + assertTrue(allRules2.contains(rule1)); + assertTrue(allRules2.contains(rule2)); + assertTrue(allRules2.contains(rule3)); + + List allRules3 = this.ruleService.getRules(nodeRef3, true); + assertNotNull(allRules3); + assertEquals(3, allRules3.size()); + assertTrue(allRules3.contains(rule1)); + assertTrue(allRules3.contains(rule2)); + assertTrue(allRules3.contains(rule3)); + } + + /** + * Ensures that rules are not duplicated when inherited + */ + public void testRuleDuplication() + { + NodeRef nodeRef1 = createNewNode(this.rootNodeRef, true); + NodeRef nodeRef2 = createNewNode(nodeRef1, true); + NodeRef nodeRef3 = createNewNode(nodeRef2, true); + NodeRef nodeRef4 = createNewNode(nodeRef1, true); + this.nodeService.addChild(nodeRef4, nodeRef3, ContentModel.ASSOC_CHILDREN, QName.createQName("{test}test")); + + Rule rule1 = createTestRule(true); + this.ruleService.saveRule(nodeRef1, rule1); + Rule rule2 = createTestRule(true); + this.ruleService.saveRule(nodeRef2, rule2); + Rule rule3 = createTestRule(true); + this.ruleService.saveRule(nodeRef3, rule3); + Rule rule4 = createTestRule(true); + this.ruleService.saveRule(nodeRef4, rule4); + + List allRules1 = this.ruleService.getRules(nodeRef1, true); + assertNotNull(allRules1); + assertEquals(1, allRules1.size()); + assertTrue(allRules1.contains(rule1)); + + List allRules2 = this.ruleService.getRules(nodeRef2, true); + assertNotNull(allRules2); + assertEquals(2, allRules2.size()); + assertTrue(allRules2.contains(rule1)); + assertTrue(allRules2.contains(rule2)); + + List allRules3 = this.ruleService.getRules(nodeRef3, true); + assertNotNull(allRules3); + assertEquals(4, allRules3.size()); + assertTrue(allRules3.contains(rule1)); + assertTrue(allRules3.contains(rule2)); + assertTrue(allRules3.contains(rule3)); + assertTrue(allRules3.contains(rule4)); + + List allRules4 = this.ruleService.getRules(nodeRef4, true); + assertNotNull(allRules4); + assertEquals(2, allRules4.size()); + assertTrue(allRules4.contains(rule1)); + assertTrue(allRules4.contains(rule4)); + } + + public void testCyclicRules() + { + } + + public void testCyclicAsyncRules() throws Exception + { + NodeRef nodeRef = createNewNode(this.rootNodeRef, true); + + // Create the first rule + + Map conditionProps = new HashMap(); + conditionProps.put(ComparePropertyValueEvaluator.PARAM_VALUE, "*.jpg"); + + Map actionProps = new HashMap(); + actionProps.put(ImageTransformActionExecuter.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_GIF); + actionProps.put(ImageTransformActionExecuter.PARAM_DESTINATION_FOLDER, nodeRef); + actionProps.put(ImageTransformActionExecuter.PARAM_ASSOC_TYPE_QNAME, ContentModel.ASSOC_CHILDREN); + actionProps.put(ImageTransformActionExecuter.PARAM_ASSOC_QNAME, ContentModel.ASSOC_CHILDREN); + + Rule rule = this.ruleService.createRule(this.ruleType.getName()); + rule.setTitle("Convert from *.jpg to *.gif"); + rule.setExecuteAsynchronously(true); + + ActionCondition actionCondition = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + actionCondition.setParameterValues(conditionProps); + rule.addActionCondition(actionCondition); + + Action action = this.actionService.createAction(ImageTransformActionExecuter.NAME); + action.setParameterValues(actionProps); + rule.addAction(action); + + // Create the next rule + + Map conditionProps2 = new HashMap(); + conditionProps2.put(ComparePropertyValueEvaluator.PARAM_VALUE, "*.gif"); + + Map actionProps2 = new HashMap(); + actionProps2.put(ImageTransformActionExecuter.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_JPEG); + actionProps2.put(ImageTransformActionExecuter.PARAM_DESTINATION_FOLDER, nodeRef); + actionProps2.put(ImageTransformActionExecuter.PARAM_ASSOC_QNAME, ContentModel.ASSOC_CHILDREN); + + Rule rule2 = this.ruleService.createRule(this.ruleType.getName()); + rule2.setTitle("Convert from *.gif to *.jpg"); + rule2.setExecuteAsynchronously(true); + + ActionCondition actionCondition2 = this.actionService.createActionCondition(ComparePropertyValueEvaluator.NAME); + actionCondition2.setParameterValues(conditionProps2); + rule2.addActionCondition(actionCondition2); + + Action action2 = this.actionService.createAction(ImageTransformActionExecuter.NAME); + action2.setParameterValues(actionProps2); + rule2.addAction(action2); + + // Save the rules + this.ruleService.saveRule(nodeRef, rule); + this.ruleService.saveRule(nodeRef, rule); + + // Now create new content + NodeRef contentNode = this.nodeService.createNode(nodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}testnode"), + ContentModel.TYPE_CONTENT).getChildRef(); + this.nodeService.setProperty(contentNode, ContentModel.PROP_NAME, "myFile.jpg"); + File file = AbstractContentTransformerTest.loadQuickTestFile("jpg"); + ContentWriter writer = this.contentService.getWriter(contentNode, ContentModel.PROP_CONTENT, true); + writer.setEncoding("UTF-8"); + writer.setMimetype(MimetypeMap.MIMETYPE_IMAGE_JPEG); + writer.putContent(file); + + setComplete(); + endTransaction(); + + //final NodeRef finalNodeRef = nodeRef; + + // Check to see what has happened +// ActionServiceImplTest.postAsyncActionTest( +// this.transactionService, +// 10000, +// 10, +// new AsyncTest() +// { +// public boolean executeTest() +// { +// List assocs = RuleServiceImplTest.this.nodeService.getChildAssocs(finalNodeRef); +// for (ChildAssociationRef ref : assocs) +// { +// NodeRef child = ref.getChildRef(); +// System.out.println("Child name: " + RuleServiceImplTest.this.nodeService.getProperty(child, ContentModel.PROP_NAME)); +// } +// +// return true; +// }; +// }); + } +} diff --git a/source/java/org/alfresco/repo/rule/RuleTestSuite.java b/source/java/org/alfresco/repo/rule/RuleTestSuite.java new file mode 100644 index 0000000000..804a4e336a --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleTestSuite.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import org.alfresco.repo.rule.ruletrigger.RuleTriggerTest; + +import junit.framework.Test; +import junit.framework.TestSuite; + + +/** + * Version test suite + * + * @author Roy Wetherall + */ +public class RuleTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(RuleTypeImplTest.class); + suite.addTestSuite(RuleTriggerTest.class); + suite.addTestSuite(RuleServiceImplTest.class); + suite.addTestSuite(RuleServiceCoverageTest.class); + return suite; + } +} diff --git a/source/java/org/alfresco/repo/rule/RuleTransactionListener.java b/source/java/org/alfresco/repo/rule/RuleTransactionListener.java new file mode 100644 index 0000000000..ad8bd4b62b --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleTransactionListener.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.util.GUID; + +/** + * The rule service transaction listener + * + * @author Roy Wetherall + */ +public class RuleTransactionListener implements TransactionListener +{ + /** + * Id used in equals and hash + */ + private String id = GUID.generate(); + + /** + * The rule service (runtime interface) + */ + private RuntimeRuleService ruleService; + + /** + * Constructor + * + * @param + */ + public RuleTransactionListener(RuntimeRuleService ruleService) + { + this.ruleService = ruleService; + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#flush() + */ + public void flush() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#beforeCommit(boolean) + */ + public void beforeCommit(boolean readOnly) + { + this.ruleService.executePendingRules(); + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#beforeCompletion() + */ + public void beforeCompletion() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#afterCommit() + */ + public void afterCommit() + { + } + + /** + * @see org.alfresco.repo.transaction.TransactionListener#afterRollback() + */ + public void afterRollback() + { + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() + { + return this.id.hashCode(); + } + + /** + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof RuleTransactionListener) + { + RuleTransactionListener that = (RuleTransactionListener) obj; + return (this.id.equals(that.id)); + } + else + { + return false; + } + } + +} diff --git a/source/java/org/alfresco/repo/rule/RuleTypeImpl.java b/source/java/org/alfresco/repo/rule/RuleTypeImpl.java new file mode 100644 index 0000000000..8045eea043 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleTypeImpl.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.util.List; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.repo.action.CommonResourceAbstractBase; +import org.alfresco.repo.rule.ruletrigger.RuleTrigger; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleType; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Rule type implementation class. + * + * @author Roy Wetherall + */ +public class RuleTypeImpl extends CommonResourceAbstractBase implements RuleType +{ + /** + * The logger + */ + private static Log logger = LogFactory.getLog(RuleTypeImpl.class); + + /** + * The action service + */ + private ActionService actionService; + + /** + * The rule service + */ + private RuleService ruleService; + + /** + * Constructor + * + * @param ruleTriggers the rule triggers + */ + public RuleTypeImpl(List ruleTriggers) + { + if (ruleTriggers != null) + { + for (RuleTrigger trigger : ruleTriggers) + { + trigger.registerRuleType(this); + } + } + } + + /** + * Set the action service + * + * @param actionService the action service + */ + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set the rule service + * + * @param ruleService the rule service + */ + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + /** + * Rule type initialise method + */ + public void init() + { + ((RuntimeRuleService)this.ruleService).registerRuleType(this); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleType#getName() + */ + public String getName() + { + return this.name; + } + + /** + * @see org.alfresco.service.cmr.rule.RuleType#getDisplayLabel() + */ + public String getDisplayLabel() + { + return I18NUtil.getMessage(this.name + "." + "display-label"); + } + + /** + * @see org.alfresco.service.cmr.rule.RuleType#triggerRuleType(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + public void triggerRuleType(NodeRef nodeRef, NodeRef actionedUponNodeRef) + { + if (this.ruleService.hasRules(nodeRef) == true) + { + List rules = this.ruleService.getRules( + nodeRef, + true, + this.name); + + for (Rule rule : rules) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Triggering rule " + rule.getId()); + } + + if (rule.getExecuteAsychronously() == true) + { + // Execute the rule now since it will be be queued for async execution later + this.actionService.executeAction(rule, actionedUponNodeRef); + } + else + { + // Queue the rule to be executed at the end of the transaction (but still in the transaction) + ((RuntimeRuleService)this.ruleService).addRulePendingExecution(nodeRef, actionedUponNodeRef, rule); + } + } + } + else + { + if (logger.isDebugEnabled() == true) + { + logger.debug("This node has no rules to trigger."); + } + } + } + + /** + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) + */ + public void setBeanName(String name) + { + this.name = name; + } +} diff --git a/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java b/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java new file mode 100644 index 0000000000..d39456e763 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuleTypeImplTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.rule.ruletrigger.RuleTrigger; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.BaseSpringTest; + +/** + * Parameter definition implementation unit test. + * + * @author Roy Wetherall + */ +public class RuleTypeImplTest extends BaseSpringTest +{ + private static final String NAME = "name"; + + private NodeService nodeService; + private ContentService contentService; + + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + + this.testStoreRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + } + + public void testConstructor() + { + create(); + } + + private RuleTypeImpl create() + { + RuleTypeImpl temp = new RuleTypeImpl(null); + temp.setBeanName(NAME); + assertNotNull(temp); + return temp; + } + + public void testGetName() + { + RuleTypeImpl temp = create(); + assertEquals(NAME, temp.getName()); + } + + // TODO Test the display label, ensuring that the label is retrieved from the resource + + // TODO Test setRuleTriggers + + // TODO Test triggerRuleType + + public void testMockInboundRuleType() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT).getChildRef(); + NodeRef nodeRef2 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + List triggers = new ArrayList(2); + triggers.add((RuleTrigger)this.applicationContext.getBean("on-content-update-trigger")); + triggers.add((RuleTrigger)this.applicationContext.getBean("on-create-child-association-trigger")); + + ExtendedRuleType ruleType = new ExtendedRuleType(triggers); + assertFalse(ruleType.rulesTriggered); + + // Update some content in order to trigger the rule type + ContentWriter contentWriter = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.putContent("any old content"); + assertTrue(ruleType.rulesTriggered); + + // Reset + ruleType.rulesTriggered = false; + assertFalse(ruleType.rulesTriggered); + + // Create a child association in order to trigger the rule type + this.nodeService.addChild( + nodeRef2, + nodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN); + assertTrue(ruleType.rulesTriggered); + } + + private class ExtendedRuleType extends RuleTypeImpl + { + public boolean rulesTriggered = false; + + public ExtendedRuleType(List ruleTriggers) + { + super(ruleTriggers); + } + + @Override + public void triggerRuleType(NodeRef nodeRef, NodeRef actionedUponNodeRef) + { + this.rulesTriggered = true; + } + + } +} diff --git a/source/java/org/alfresco/repo/rule/RulesAspect.java b/source/java/org/alfresco/repo/rule/RulesAspect.java new file mode 100644 index 0000000000..e3a4294676 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RulesAspect.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.Behaviour; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * Class containing behaviour for the rules aspect + * + * @author Roy Wetherall + */ +public class RulesAspect +{ + private Behaviour onAddAspectBehaviour; + + private PolicyComponent policyComponent; + + private RuleService ruleService; + + private NodeService nodeService; + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + public void init() + { + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + RuleModel.ASPECT_RULES, + new JavaBehaviour(this, "onCopyNode")); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"), + RuleModel.ASPECT_RULES, + new JavaBehaviour(this, "onCopyComplete")); + + this.onAddAspectBehaviour = new JavaBehaviour(this, "onAddAspect"); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + RuleModel.ASPECT_RULES, + onAddAspectBehaviour); + } + + /** + * Helper to diable the on add aspect policy behaviour. Helpful when importing, + * copying and other bulk respstorative operations. + * + * TODO will eventually be redundant when policies can be enabled/diabled in the + * policy componenet + */ + public void disbleOnAddAspect() + { + this.onAddAspectBehaviour.disable(); + } + + /** + * Helper to enable the on add aspect policy behaviour. Helpful when importing, + * copying and other bulk respstorative operations. + * + * TODO will eventually be redundant when policies can be enabled/diabled in the + * policy componenet + */ + public void enableOnAddAspect() + { + this.onAddAspectBehaviour.enable(); + } + + /** + * On add aspect policy behaviour + * @param nodeRef + * @param aspectTypeQName + */ + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + this.ruleService.disableRules(nodeRef); + try + { + this.nodeService.createNode( + nodeRef, + RuleModel.ASSOC_RULE_FOLDER, + RuleModel.ASSOC_RULE_FOLDER, + ContentModel.TYPE_SYSTEM_FOLDER); + } + finally + { + this.ruleService.enableRules(nodeRef); + } + } + + public void onCopyNode( + QName classRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + copyDetails.addAspect(RuleModel.ASPECT_RULES); + + List assocs = this.nodeService.getChildAssocs( + sourceNodeRef, + RegexQNamePattern.MATCH_ALL, + RuleModel.ASSOC_RULE_FOLDER); + for (ChildAssociationRef assoc : assocs) + { + copyDetails.addChildAssociation(classRef, assoc, true); + } + + this.onAddAspectBehaviour.disable(); + } + + public void onCopyComplete( + QName classRef, + NodeRef sourceNodeRef, + NodeRef destinationRef, + Map copyMap) + { + this.onAddAspectBehaviour.enable(); + } +} diff --git a/source/java/org/alfresco/repo/rule/RuntimeRuleService.java b/source/java/org/alfresco/repo/rule/RuntimeRuleService.java new file mode 100644 index 0000000000..bf447bf10d --- /dev/null +++ b/source/java/org/alfresco/repo/rule/RuntimeRuleService.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleType; + +/** + * @author Roy Wetherall + */ +public interface RuntimeRuleService +{ + void addRulePendingExecution(NodeRef actionableNodeRef, NodeRef actionedUponNodeRef, Rule rule); + + void addRulePendingExecution(NodeRef actionableNodeRef, NodeRef actionedUponNodeRef, Rule rule, boolean executeAtEnd); + + void executePendingRules(); + + void registerRuleType(RuleType ruleType); +} diff --git a/source/java/org/alfresco/repo/rule/ruleModel.xml b/source/java/org/alfresco/repo/rule/ruleModel.xml new file mode 100644 index 0000000000..053472cdaf --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruleModel.xml @@ -0,0 +1,55 @@ + + + Alfresco Rule Model + Alfresco + 2005-08-16 + 0.1 + + + + + + + + + + + + + + + + Rule + act:compositeaction + + + d:text + true + + + d:boolean + true + + + + + + + + + + Rules + + + + cm:systemfolder + false + false + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java new file mode 100644 index 0000000000..6caa2ed7fb --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/CreateNodeRuleTrigger.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * We use this specialised trigger for create node beaucse of a problem with the CIFS integration. + *

    + * The create node trigger will only be fired if the object is NOT a sub-type of content. + * + * @author Roy Wetherall + */ +public class CreateNodeRuleTrigger extends SingleChildAssocRefPolicyRuleTrigger +{ + /** + * The logger + */ + private static Log logger = LogFactory.getLog(CreateNodeRuleTrigger.class); + + DictionaryService dictionaryService; + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void policyBehaviour(ChildAssociationRef childAssocRef) + { + // Only fire the rule if the node is question has no potential to contain content + // TODO we need to find a better way to do this .. how can this be resolved in CIFS?? + boolean triggerRule = false; + QName type = this.nodeService.getType(childAssocRef.getChildRef()); + ClassDefinition classDefinition = this.dictionaryService.getClass(type); + if (classDefinition != null) + { + for (PropertyDefinition propertyDefinition : classDefinition.getProperties().values()) + { + if (propertyDefinition.getDataType().getName().equals(DataTypeDefinition.CONTENT) == true) + { + triggerRule = true; + break; + } + } + } + + if (triggerRule == false) + { + if (logger.isDebugEnabled() == true) + { + logger.debug( + "Create node rule trigger fired for parent node " + + this.nodeService.getType(childAssocRef.getParentRef()).toString() + " " + childAssocRef.getParentRef() + + " and child node " + + this.nodeService.getType(childAssocRef.getChildRef()).toString() + " " + childAssocRef.getChildRef()); + } + + triggerRules(childAssocRef.getParentRef(), childAssocRef.getChildRef()); + } + } +} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/RuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTrigger.java new file mode 100644 index 0000000000..579861000a --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTrigger.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import org.alfresco.service.cmr.rule.RuleType; + +/** + * Rule trigger interface + * + * @author Roy Wetherall + */ +public interface RuleTrigger +{ + /** + * Register the rule trigger + */ + void registerRuleTrigger(); + + /** + * Register the rule type as using this trigger + * + * @param ruleType the rule type + */ + void registerRuleType(RuleType ruleType); +} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerAbstractBase.java b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerAbstractBase.java new file mode 100644 index 0000000000..beec32b61f --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerAbstractBase.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleType; + +/** + * Rule trigger abstract base + * + * @author Roy Wetherall + */ +public abstract class RuleTriggerAbstractBase implements RuleTrigger +{ + /** + * A list of the rule types that are interested in this trigger + */ + private Set ruleTypes = new HashSet(); + + /** + * The policy component + */ + protected PolicyComponent policyComponent; + + /** + * The node service + */ + protected NodeService nodeService; + + /** + * The authentication Component + */ + + protected AuthenticationComponent authenticationComponent; + + /** + * Set the policy component + * + * @param policyComponent + * the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the node service + * + * @param nodeService + * the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the authenticationComponent + */ + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + /** + * Registration of an interested rule type + */ + public void registerRuleType(RuleType ruleType) + { + this.ruleTypes.add(ruleType); + } + + /** + * Trigger the rules that relate to any interested rule types for the node + * references passed. + * + * @param nodeRef + * the node reference who rules are to be triggered + * @param actionedUponNodeRef + * the node reference that will be actioned upon by the rules + */ + protected void triggerRules(NodeRef nodeRef, NodeRef actionedUponNodeRef) + { + String userName = authenticationComponent.getCurrentUserName(); + authenticationComponent.setSystemUserAsCurrentUser(); + try + { + for (RuleType ruleType : this.ruleTypes) + { + ruleType.triggerRuleType(nodeRef, actionedUponNodeRef); + } + } + finally + { + authenticationComponent.clearCurrentSecurityContext(); + if(userName != null) + { + authenticationComponent.setCurrentUser(userName); + } + } + } +} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java new file mode 100644 index 0000000000..4aaf06edcc --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/RuleTriggerTest.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.util.BaseSpringTest; + +/** + * Rule trigger test + * + * @author Roy Wetherall + */ +public class RuleTriggerTest extends BaseSpringTest +{ + private static final String ON_CREATE_NODE_TRIGGER = "on-create-node-trigger"; + private static final String ON_UPDATE_NODE_TRIGGER = "on-update-node-trigger"; + private static final String ON_DELETE_NODE_TRIGGER = "on-delete-node-trigger"; + private static final String ON_CREATE_CHILD_ASSOCIATION_TRIGGER = "on-create-child-association-trigger"; + private static final String ON_DELETE_CHILD_ASSOCIATION_TRIGGER = "on-delete-child-association-trigger"; + private static final String ON_CREATE_ASSOCIATION_TRIGGER = "on-create-association-trigger"; + private static final String ON_DELETE_ASSOCIATION_TRIGGER = "on-delete-association-trigger"; + private static final String ON_CONTENT_UPDATE_TRIGGER = "on-content-update-trigger"; + + private NodeService nodeService; + private ContentService contentService; + + private StoreRef testStoreRef; + private NodeRef rootNodeRef; + + @Override + protected void onSetUpInTransaction() throws Exception + { + this.nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + + this.testStoreRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); + } + + public void testOnCreateNodeTrigger() + { + TestRuleType ruleType = createTestRuleType(ON_CREATE_NODE_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnUpdateNodeTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + TestRuleType ruleType = createTestRuleType(ON_UPDATE_NODE_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.setProperty(nodeRef, ContentModel.PROP_NAME, "nameChanged"); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnDeleteNodeTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + TestRuleType ruleType = createTestRuleType(ON_DELETE_NODE_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.deleteNode(nodeRef); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnCreateChildAssociationTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef nodeRef2 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + TestRuleType ruleType = createTestRuleType(ON_CREATE_CHILD_ASSOCIATION_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.addChild( + nodeRef, + nodeRef2, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnDeleteChildAssociationTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef nodeRef2 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + this.nodeService.addChild( + nodeRef, + nodeRef2, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN); + + TestRuleType ruleType = createTestRuleType(ON_DELETE_CHILD_ASSOCIATION_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.removeChild(nodeRef, nodeRef2); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnCreateAssociationTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef nodeRef2 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + + TestRuleType ruleType = createTestRuleType(ON_CREATE_ASSOCIATION_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.createAssociation(nodeRef, nodeRef2, ContentModel.ASSOC_CHILDREN); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnDeleteAssociationTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef nodeRef2 = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTAINER).getChildRef(); + this.nodeService.createAssociation(nodeRef, nodeRef2, ContentModel.ASSOC_CHILDREN); + + TestRuleType ruleType = createTestRuleType(ON_DELETE_ASSOCIATION_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + this.nodeService.removeAssociation(nodeRef, nodeRef2, ContentModel.ASSOC_CHILDREN); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + public void testOnContentUpdateTrigger() + { + NodeRef nodeRef = this.nodeService.createNode( + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT).getChildRef(); + + TestRuleType ruleType = createTestRuleType(ON_CONTENT_UPDATE_TRIGGER); + assertFalse(ruleType.rulesTriggered); + + // Try and trigger the type + ContentWriter contentWriter = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.setEncoding("UTF-8"); + contentWriter.putContent("some content"); + + // Check to see if the rule type has been triggered + assertTrue(ruleType.rulesTriggered); + } + + private TestRuleType createTestRuleType(String ruleTriggerName) + { + RuleTrigger ruleTrigger = (RuleTrigger)this.applicationContext.getBean(ruleTriggerName); + assertNotNull(ruleTrigger); + TestRuleType ruleType = new TestRuleType(); + ruleTrigger.registerRuleType(ruleType); + return ruleType; + } + + private class TestRuleType implements RuleType + { + public boolean rulesTriggered = false; + + public String getName() + { + return "testRuleType"; + } + + public String getDisplayLabel() + { + return "displayLabel"; + } + + public void triggerRuleType(NodeRef nodeRef, NodeRef actionedUponNodeRef) + { + // Indicate that the rules have been triggered + this.rulesTriggered = true; + } + } +} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/SingleAssocRefPolicyRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/SingleAssocRefPolicyRuleTrigger.java new file mode 100644 index 0000000000..e9e2075221 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/SingleAssocRefPolicyRuleTrigger.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +public class SingleAssocRefPolicyRuleTrigger extends RuleTriggerAbstractBase +{ + private static final String ERR_POLICY_NAME_NOT_SET = "Unable to register rule trigger since policy name has not been set."; + + private String policyNamespace = NamespaceService.ALFRESCO_URI; + + private String policyName; + + public void setPolicyNamespace(String policyNamespace) + { + this.policyNamespace = policyNamespace; + } + + public void setPolicyName(String policyName) + { + this.policyName = policyName; + } + + /** + * @see org.alfresco.repo.rule.ruletrigger.RuleTrigger#registerRuleTrigger() + */ + public void registerRuleTrigger() + { + if (policyName == null) + { + throw new RuleServiceException(ERR_POLICY_NAME_NOT_SET); + } + + this.policyComponent.bindAssociationBehaviour( + QName.createQName(this.policyNamespace, this.policyName), + this, + new JavaBehaviour(this, "policyBehaviour")); + } + + public void policyBehaviour(AssociationRef assocRef) + { + triggerRules(assocRef.getSourceRef(), assocRef.getTargetRef()); + } +} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/SingleChildAssocRefPolicyRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/SingleChildAssocRefPolicyRuleTrigger.java new file mode 100644 index 0000000000..8a911c7ac7 --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/SingleChildAssocRefPolicyRuleTrigger.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class SingleChildAssocRefPolicyRuleTrigger extends RuleTriggerAbstractBase +{ + private static final String ERR_POLICY_NAME_NOT_SET = "Unable to register rule trigger since policy name has not been set."; + + /** + * The logger + */ + private static Log logger = LogFactory.getLog(SingleChildAssocRefPolicyRuleTrigger.class); + + private String policyNamespace = NamespaceService.ALFRESCO_URI; + + private String policyName; + + private boolean isClassBehaviour = false; + + public void setPolicyNamespace(String policyNamespace) + { + this.policyNamespace = policyNamespace; + } + + public void setPolicyName(String policyName) + { + this.policyName = policyName; + } + + public void setIsClassBehaviour(boolean isClassBehaviour) + { + this.isClassBehaviour = isClassBehaviour; + } + + /** + * @see org.alfresco.repo.rule.ruletrigger.RuleTrigger#registerRuleTrigger() + */ + public void registerRuleTrigger() + { + if (policyName == null) + { + throw new RuleServiceException(ERR_POLICY_NAME_NOT_SET); + } + + if (isClassBehaviour == true) + { + this.policyComponent.bindClassBehaviour( + QName.createQName(this.policyNamespace, this.policyName), + this, + new JavaBehaviour(this, "policyBehaviour")); + } + else + { + this.policyComponent.bindAssociationBehaviour( + QName.createQName(this.policyNamespace, this.policyName), + this, + new JavaBehaviour(this, "policyBehaviour")); + } + } + + public void policyBehaviour(ChildAssociationRef childAssocRef) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Single child assoc trigger (policy = " + this.policyName + ") fired for parent node " + childAssocRef.getParentRef() + " and child node " + childAssocRef.getChildRef()); + } + + triggerRules(childAssocRef.getParentRef(), childAssocRef.getChildRef()); + } +} diff --git a/source/java/org/alfresco/repo/rule/ruletrigger/SingleNodeRefPolicyRuleTrigger.java b/source/java/org/alfresco/repo/rule/ruletrigger/SingleNodeRefPolicyRuleTrigger.java new file mode 100644 index 0000000000..432369f5ab --- /dev/null +++ b/source/java/org/alfresco/repo/rule/ruletrigger/SingleNodeRefPolicyRuleTrigger.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.rule.ruletrigger; + +import java.util.List; + +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +public class SingleNodeRefPolicyRuleTrigger extends RuleTriggerAbstractBase +{ + private static final String ERR_POLICY_NAME_NOT_SET = "Unable to register rule trigger since policy name has not been set."; + + private String policyNamespace = NamespaceService.ALFRESCO_URI; + + private String policyName; + + private boolean triggerParentRules = true; + + public void setPolicyNamespace(String policyNamespace) + { + this.policyNamespace = policyNamespace; + } + + public void setPolicyName(String policyName) + { + this.policyName = policyName; + } + + public void setTriggerParentRules(boolean triggerParentRules) + { + this.triggerParentRules = triggerParentRules; + } + + public void registerRuleTrigger() + { + if (policyName == null) + { + throw new RuleServiceException(ERR_POLICY_NAME_NOT_SET); + } + + this.policyComponent.bindClassBehaviour( + QName.createQName(this.policyNamespace, this.policyName), + this, + new JavaBehaviour(this, "policyBehaviour")); + } + + public void policyBehaviour(NodeRef nodeRef) + { + if (triggerParentRules == true) + { + List parentsAssocRefs = this.nodeService.getParentAssocs(nodeRef); + for (ChildAssociationRef parentAssocRef : parentsAssocRefs) + { + triggerRules(parentAssocRef.getParentRef(), nodeRef); + } + } + else + { + triggerRules(nodeRef, nodeRef); + } + } +} diff --git a/source/java/org/alfresco/repo/search/AbstractResultSet.java b/source/java/org/alfresco/repo/search/AbstractResultSet.java new file mode 100644 index 0000000000..face6ffae3 --- /dev/null +++ b/source/java/org/alfresco/repo/search/AbstractResultSet.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; + +public abstract class AbstractResultSet implements ResultSet +{ + + private Path[] propertyPaths; + + public AbstractResultSet(Path[] propertyPaths) + { + super(); + this.propertyPaths = propertyPaths; + } + + public Path[] getPropertyPaths() + { + return propertyPaths; + } + + + public float getScore(int n) + { + // All have equal weight by default + return 1.0f; + } + + public void close() + { + // default to do nothing + } + + public List getNodeRefs() + { + ArrayList nodeRefs = new ArrayList(length()); + for(ResultSetRow row: this) + { + nodeRefs.add(row.getNodeRef()); + } + return nodeRefs; + } + + public List getChildAssocRefs() + { + ArrayList cars = new ArrayList(length()); + for(ResultSetRow row: this) + { + cars.add(row.getChildAssocRef()); + } + return cars; + } + + + +} diff --git a/source/java/org/alfresco/repo/search/AbstractResultSetRow.java b/source/java/org/alfresco/repo/search/AbstractResultSetRow.java new file mode 100644 index 0000000000..32368f3318 --- /dev/null +++ b/source/java/org/alfresco/repo/search/AbstractResultSetRow.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.namespace.QName; + +public abstract class AbstractResultSetRow implements ResultSetRow +{ + + /** + * The containing result set + */ + private ResultSet resultSet; + + /** + * The current position in the containing result set + */ + private int index; + + /** + * The direct properties of the current node + * Used by those implementations that can cache the whole set. + */ + + private Map properties; + + public AbstractResultSetRow(ResultSet resultSet, int index) + { + super(); + this.resultSet = resultSet; + this.index = index; + } + + public ResultSet getResultSet() + { + return resultSet; + } + + public int getIndex() + { + return index; + } + + public NodeRef getNodeRef() + { + return getResultSet().getNodeRef(getIndex()); + } + + public float getScore() + { + return getResultSet().getScore(getIndex()); + } + + public Map getValues() + { + if (properties == null) + { + properties = new HashMap(); + setProperties(getDirectProperties()); + } + return Collections.unmodifiableMap(properties); + } + + protected Map getDirectProperties() + { + return Collections.emptyMap(); + } + + protected void setProperties(Map byQname) + { + for (QName qname : byQname.keySet()) + { + Serializable value = byQname.get(qname); + Path path = new Path(); + path.append(new Path.SelfElement()); + path.append(new Path.AttributeElement(qname)); + properties.put(path, value); + } + } + + public Serializable getValue(QName qname) + { + Path path = new Path(); + path.append(new Path.SelfElement()); + path.append(new Path.AttributeElement(qname)); + return getValues().get(path); + } + +} diff --git a/source/java/org/alfresco/repo/search/AbstractResultSetRowIterator.java b/source/java/org/alfresco/repo/search/AbstractResultSetRowIterator.java new file mode 100644 index 0000000000..7b06e8162b --- /dev/null +++ b/source/java/org/alfresco/repo/search/AbstractResultSetRowIterator.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; + + +/** + * Iterate over the rows in a ResultSet + * + * @author andyh + * + */ +public abstract class AbstractResultSetRowIterator implements ResultSetRowIterator +{ + /** + * The result set + */ + private ResultSet resultSet; + + /** + * The current position + */ + private int position = -1; + + /** + * The maximum position + */ + private int max; + + /** + * Create an iterator over the result set. Follows stadard ListIterator + * conventions + * + * @param resultSet + */ + public AbstractResultSetRowIterator(ResultSet resultSet) + { + super(); + this.resultSet = resultSet; + this.max = resultSet.length(); + } + + + + public ResultSet getResultSet() + { + return resultSet; + } + + + + + /* + * ListIterator implementation + */ + public boolean hasNext() + { + return position < (max - 1); + } + + public boolean allowsReverse() + { + return true; + } + + public boolean hasPrevious() + { + return position > 0; + } + + abstract public ResultSetRow next(); + + protected int moveToNextPosition() + { + return ++position; + } + + abstract public ResultSetRow previous(); + + protected int moveToPreviousPosition() + { + return --position; + } + + public int nextIndex() + { + return position + 1; + } + + public int previousIndex() + { + return position - 1; + } + + /* + * Mutation is not supported + */ + + public void remove() + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void set(ResultSetRow o) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void add(ResultSetRow o) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + +} diff --git a/source/java/org/alfresco/repo/search/AbstractSearcherComponent.java b/source/java/org/alfresco/repo/search/AbstractSearcherComponent.java new file mode 100644 index 0000000000..3f519536fd --- /dev/null +++ b/source/java/org/alfresco/repo/search/AbstractSearcherComponent.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.XPathException; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.util.SearchLanguageConversion; + +/** + * Provides basic searcher support + * + * @author Andy Hind + */ +public abstract class AbstractSearcherComponent implements SearchService +{ + /** + * Not implemented, but will eventually map directly to + * {@link SearchLanguageConversion}. + */ + protected String translateQuery(String fromLanguage, String toLangage, String query) + { + throw new UnsupportedOperationException(); + } + + public ResultSet query(StoreRef store, String language, String query) + { + return query(store, language, query, null, null); + } + + public ResultSet query(StoreRef store, String language, String query, + QueryParameterDefinition[] queryParameterDefintions) + { + return query(store, language, query, null, queryParameterDefintions); + } + + public ResultSet query(StoreRef store, String language, String query, Path[] attributePaths) + { + return query(store, language, query, attributePaths, null); + } + + public List selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, + NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + throws InvalidNodeRefException, XPathException + { + return selectNodes(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks, + SearchService.LANGUAGE_XPATH); + } + + public List selectProperties(NodeRef contextNodeRef, String xpath, + QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks) throws InvalidNodeRefException, XPathException + { + return selectProperties(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks, + SearchService.LANGUAGE_XPATH); + } +} diff --git a/source/java/org/alfresco/repo/search/CannedQueryDef.java b/source/java/org/alfresco/repo/search/CannedQueryDef.java new file mode 100644 index 0000000000..9c2fcc0ae1 --- /dev/null +++ b/source/java/org/alfresco/repo/search/CannedQueryDef.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.Collection; +import java.util.Map; + +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + +/** + * The definition of a canned query + * + * @author andyh + * + */ +public interface CannedQueryDef +{ + /** + * Get the unique name for the query + * + * @return + */ + public QName getQname(); + + /** + * Get the language in which the query is defined. + * + * @return + */ + public String getLanguage(); + + /** + * Get the definitions for any query parameters. + * + * @return + */ + public Collection getQueryParameterDefs(); + + /** + * Get the query string. + * + * @return + */ + public String getQuery(); + + /** + * Return the mechanism that this query definition uses to map namespace + * prefixes to URIs. A query may use a predefined set of prefixes for known + * URIs. I would be unwise to rely on the defaults. + * + * @return + */ + public NamespacePrefixResolver getNamespacePrefixResolver(); + + /** + * Get a map to look up definitions by Qname + * + * @return + */ + public Map getQueryParameterMap(); +} diff --git a/source/java/org/alfresco/repo/search/CannedQueryDefImpl.java b/source/java/org/alfresco/repo/search/CannedQueryDefImpl.java new file mode 100644 index 0000000000..ab6332a18c --- /dev/null +++ b/source/java/org/alfresco/repo/search/CannedQueryDefImpl.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.search.NamedQueryParameterDefinition; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.dom4j.Element; +import org.dom4j.Namespace; + +public class CannedQueryDefImpl implements CannedQueryDef +{ + private static final org.dom4j.QName ELEMENT_QNAME = new org.dom4j.QName("query-definition", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName QNAME = new org.dom4j.QName("qname", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName LANGUAGE = new org.dom4j.QName("language", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName QUERY = new org.dom4j.QName("query", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private QName qName; + + private String language; + + private Map queryParameterDefs = new HashMap(); + + String query; + + QueryCollection container; + + public CannedQueryDefImpl(QName qName, String language, String query, List queryParameterDefs, QueryCollection container) + { + super(); + this.qName = qName; + this.language = language; + this.query = query; + for(QueryParameterDefinition paramDef : queryParameterDefs) + { + this.queryParameterDefs.put(paramDef.getQName(), paramDef); + } + this.container = container; + } + + public QName getQname() + { + return qName; + } + + public String getLanguage() + { + return language; + } + + public Collection getQueryParameterDefs() + { + return Collections.unmodifiableCollection(queryParameterDefs.values()); + } + + public String getQuery() + { + return query; + } + + public NamespacePrefixResolver getNamespacePrefixResolver() + { + return container.getNamespacePrefixResolver(); + } + + public static CannedQueryDefImpl createCannedQuery(Element element, DictionaryService dictionaryService, QueryCollection container, NamespacePrefixResolver nspr) + { + if (element.getQName().getName().equals(ELEMENT_QNAME.getName())) + { + QName qName = null; + Element qNameElement = element.element(QNAME.getName()); + if(qNameElement != null) + { + qName = QName.createQName(qNameElement.getText(), container.getNamespacePrefixResolver()); + } + + String language = null; + Element languageElement = element.element(LANGUAGE.getName()); + if(languageElement != null) + { + language = languageElement.getText(); + } + + String query = null; + Element queryElement = element.element(QUERY.getName()); + if(queryElement != null) + { + query = queryElement.getText(); + } + + List queryParameterDefs = new ArrayList(); + + List list = element.elements(QueryParameterDefImpl.getElementQName().getName()); + for(Iterator it = list.iterator(); it.hasNext(); /**/) + { + Element defElement = (Element) it.next(); + NamedQueryParameterDefinition nqpd = QueryParameterDefImpl.createParameterDefinition(defElement, dictionaryService, nspr); + queryParameterDefs.add(nqpd.getQueryParameterDefinition()); + } + + list = element.elements(QueryParameterRefImpl.getElementQName().getName()); + for(Iterator it = list.iterator(); it.hasNext(); /**/) + { + Element refElement = (Element) it.next(); + NamedQueryParameterDefinition nqpd = QueryParameterRefImpl.createParameterReference(refElement, dictionaryService, container); + QueryParameterDefinition resolved = nqpd.getQueryParameterDefinition(); + if(resolved == null) + { + throw new AlfrescoRuntimeException("Unable to find refernce parameter : "+nqpd.getQName()); + } + queryParameterDefs.add(resolved); + } + + return new CannedQueryDefImpl(qName, language, query, queryParameterDefs, container); + + } + else + { + return null; + } + } + + public static org.dom4j.QName getElementQName() + { + return ELEMENT_QNAME; + } + + public Map getQueryParameterMap() + { + return Collections.unmodifiableMap(queryParameterDefs); + } + +} diff --git a/source/java/org/alfresco/repo/search/DocumentNavigator.java b/source/java/org/alfresco/repo/search/DocumentNavigator.java new file mode 100644 index 0000000000..ff04d0088e --- /dev/null +++ b/source/java/org/alfresco/repo/search/DocumentNavigator.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.jaxen.DefaultNavigator; +import org.jaxen.JaxenException; +import org.jaxen.UnsupportedAxisException; +import org.jaxen.XPath; + +/** + * An implementation of the Jaxen xpath against the node service API + * + * This means any node service can do xpath style navigation. Given any context + * node we can navigate between nodes using xpath. + * + * This allows simple path navigation and much more. + * + * @author Andy Hind + * + */ +public class DocumentNavigator extends DefaultNavigator +{ + private static QName JCR_ROOT = QName.createQName("http://www.jcp.org/jcr/1.0", "root"); + + private static QName JCR_PRIMARY_TYPE = QName.createQName("http://www.jcp.org/jcr/1.0", "primaryType"); + + private static QName JCR_MIXIN_TYPES = QName.createQName("http://www.jcp.org/jcr/1.0", "mixinTypes"); + + private static final long serialVersionUID = 3618984485740165427L; + + private DictionaryService dictionaryService; + + private NodeService nodeService; + + private SearchService searchService; + + private NamespacePrefixResolver nspr; + + // Support classes to encapsulate stuff more akin to xml + + public class Property + { + public final QName qname; + + public final Serializable value; + + public final NodeRef parent; + + public Property(QName qname, Serializable value, NodeRef parent) + { + this.qname = qname; + this.value = value; + this.parent = parent; + } + } + + + public class Namespace + { + public final String prefix; + + public final String uri; + + public Namespace(String prefix, String uri) + { + this.prefix = prefix; + this.uri = uri; + } + } + + public class JCRRootNodeChildAssociationRef extends ChildAssociationRef + { + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = -3890194577752476675L; + + public JCRRootNodeChildAssociationRef(QName assocTypeQName, NodeRef parentRef, QName childQName, NodeRef childRef) + { + super(assocTypeQName, parentRef, childQName, childRef); + } + + public JCRRootNodeChildAssociationRef(QName assocTypeQName, NodeRef parentRef, QName childQName, NodeRef childRef, boolean isPrimary, int nthSibling) + { + super(assocTypeQName, parentRef, childQName, childRef, isPrimary, nthSibling); + } + + } + + private boolean followAllParentLinks; + + private boolean useJCRRootNode; + + /** + * @param dictionaryService + * used to resolve the subtypeOf function and other + * type-related functions + * @param nodeService + * the NodeService against which to execute + * @param searchService + * the service that helps resolve functions such as like + * and contains + * @param nspr + * resolves namespaces in the xpath + * @param followAllParentLinks + * true if the XPath should traverse all parent associations when + * going up the hierarchy; false if the only the primary + * parent-child association should be traversed + */ + public DocumentNavigator(DictionaryService dictionaryService, NodeService nodeService, SearchService searchService, + NamespacePrefixResolver nspr, boolean followAllParentLinks, boolean useJCRRootNode) + { + super(); + this.dictionaryService = dictionaryService; + this.nodeService = nodeService; + this.searchService = searchService; + this.nspr = nspr; + this.followAllParentLinks = followAllParentLinks; + this.useJCRRootNode = useJCRRootNode; + } + + + + public NamespacePrefixResolver getNamespacePrefixResolver() + { + return nspr; + } + + + + /** + * Allow this to be set as it commonly changes from one search to the next + * + * @param followAllParentLinks + * true + */ + public void setFollowAllParentLinks(boolean followAllParentLinks) + { + this.followAllParentLinks = followAllParentLinks; + } + + public String getAttributeName(Object o) + { + // Get the local name + String escapedLocalName = ISO9075.encode(((Property) o).qname.getLocalName()); + if(escapedLocalName == ((Property) o).qname.getLocalName()) + { + return escapedLocalName; + } + return escapedLocalName; + } + + public String getAttributeNamespaceUri(Object o) + { + return ((Property) o).qname.getNamespaceURI(); + } + + public String getAttributeQName(Object o) + { + QName qName = ((Property) o).qname; + String escapedLocalName = ISO9075.encode(qName.getLocalName()); + if(escapedLocalName == qName.getLocalName()) + { + return qName.toString(); + } + else + { + return QName.createQName(qName.getNamespaceURI(), escapedLocalName).toString(); + } + } + + public String getAttributeStringValue(Object o) + { + // Only the first property of multi-valued properties is displayed + // A multivalue attribute makes no sense in the xml world + return DefaultTypeConverter.INSTANCE.convert(String.class, ((Property) o).value); + } + + public String getCommentStringValue(Object o) + { + // There is no attribute that is a comment + throw new UnsupportedOperationException("Comment string values are unsupported"); + } + + public String getElementName(Object o) + { + return ISO9075.encode(((ChildAssociationRef) o).getQName().getLocalName()); + } + + public String getElementNamespaceUri(Object o) + { + return ((ChildAssociationRef) o).getQName().getNamespaceURI(); + } + + public String getElementQName(Object o) + { + QName qName = ((ChildAssociationRef) o).getQName(); + String escapedLocalName = ISO9075.encode(qName.getLocalName()); + if(escapedLocalName == qName.getLocalName()) + { + return qName.toString(); + } + else + { + return QName.createQName(qName.getNamespaceURI(), escapedLocalName).toString(); + } + } + + public String getElementStringValue(Object o) + { + throw new UnsupportedOperationException("Element string values are unsupported"); + } + + public String getNamespacePrefix(Object o) + { + return ((Namespace) o).prefix; + } + + public String getNamespaceStringValue(Object o) + { + return ((Namespace) o).uri; + } + + public String getTextStringValue(Object o) + { + throw new UnsupportedOperationException("Text nodes are unsupported"); + } + + public boolean isAttribute(Object o) + { + return (o instanceof Property); + } + + public boolean isComment(Object o) + { + return false; + } + + public boolean isDocument(Object o) + { + if (!(o instanceof ChildAssociationRef)) + { + return false; + } + ChildAssociationRef car = (ChildAssociationRef) o; + return (car.getParentRef() == null) && (car.getQName() == null); + } + + public boolean isElement(Object o) + { + return (o instanceof ChildAssociationRef); + } + + public boolean isNamespace(Object o) + { + return (o instanceof Namespace); + } + + public boolean isProcessingInstruction(Object o) + { + return false; + } + + public boolean isText(Object o) + { + return false; + } + + public XPath parseXPath(String o) throws JaxenException + { + return new NodeServiceXPath(o, this, null); + } + + // Basic navigation support + + public Iterator getAttributeAxisIterator(Object o) throws UnsupportedAxisException + { + ArrayList properties = new ArrayList(); + NodeRef nodeRef = ((ChildAssociationRef) o).getChildRef(); + Map map = nodeService.getProperties(nodeRef); + for (QName qName : map.keySet()) + { + if(map.get(qName) instanceof Collection) + { + for(Serializable ob : (Collection) map.get(qName)) + { + Property property = new Property(qName, ob, nodeRef); + properties.add(property); + } + } + else + { + Property property = new Property(qName, map.get(qName), nodeRef); + properties.add(property); + } + } + if(useJCRRootNode) + { + properties.add(new Property(JCR_PRIMARY_TYPE, nodeService.getType(nodeRef), nodeRef)); + for(QName mixin : nodeService.getAspects(nodeRef)) + { + properties.add(new Property(JCR_MIXIN_TYPES, mixin, nodeRef)); + } + } + + return properties.iterator(); + } + + public Iterator getChildAxisIterator(Object o) throws UnsupportedAxisException + { + // Iterator of ChildAxisRef + ChildAssociationRef assocRef = (ChildAssociationRef) o; + NodeRef childRef = assocRef.getChildRef(); + List list; + // Add compatability for JCR 170 by including the root node. + if(isDocument(o) && useJCRRootNode) + { + list = new ArrayList(1); + list.add(new JCRRootNodeChildAssociationRef(ContentModel.ASSOC_CHILDREN, childRef, JCR_ROOT, childRef, true, 0)); + } + else + { + list = nodeService.getChildAssocs(childRef); + } + return list.iterator(); + } + + public Iterator getNamespaceAxisIterator(Object o) throws UnsupportedAxisException + { + // Iterator of Namespace + ArrayList namespaces = new ArrayList(); + for (String prefix : nspr.getPrefixes()) + { + String uri = nspr.getNamespaceURI(prefix); + Namespace ns = new Namespace(prefix, uri); + namespaces.add(ns); + } + return namespaces.iterator(); + } + + public Iterator getParentAxisIterator(Object o) throws UnsupportedAxisException + { + ArrayList parents = new ArrayList(1); + // Iterator of ?? + if (o instanceof ChildAssociationRef) + { + ChildAssociationRef contextRef = (ChildAssociationRef) o; + if (contextRef.getParentRef() != null) + { + if (followAllParentLinks) + { + for (ChildAssociationRef car : nodeService.getParentAssocs(contextRef.getChildRef())) + { + parents.add(nodeService.getPrimaryParent(car.getParentRef())); + } + } + else + { + parents.add(nodeService.getPrimaryParent(contextRef.getParentRef())); + } + } + } + if (o instanceof Property) + { + Property p = (Property) o; + parents.add(nodeService.getPrimaryParent(p.parent)); + } + return parents.iterator(); + } + + public Object getDocumentNode(Object o) + { + ChildAssociationRef assocRef = (ChildAssociationRef) o; + StoreRef storeRef = assocRef.getChildRef().getStoreRef(); + return new ChildAssociationRef(null, null, null, nodeService.getRootNode(storeRef)); + } + + public Object getNode(NodeRef nodeRef) + { + return nodeService.getPrimaryParent(nodeRef); + } + + public List getNode(NodeRef nodeRef, QNamePattern qNamePattern) + { + return nodeService.getParentAssocs(nodeRef, RegexQNamePattern.MATCH_ALL, qNamePattern); + } + + public Boolean like(NodeRef childRef, QName qname, String sqlLikePattern, boolean includeFTS) + { + return searchService.like(childRef, qname, sqlLikePattern, includeFTS); + } + + public Boolean contains(NodeRef childRef, QName qname, String sqlLikePattern, SearchParameters.Operator defaultOperator) + { + return searchService.contains(childRef, qname, sqlLikePattern, defaultOperator); + } + + public Boolean isSubtypeOf(NodeRef nodeRef, QName typeQName) + { + // get the type of the node + QName nodeTypeQName = nodeService.getType(nodeRef); + return dictionaryService.isSubClass(nodeTypeQName, typeQName); + } +} diff --git a/source/java/org/alfresco/repo/search/EmptyResultSet.java b/source/java/org/alfresco/repo/search/EmptyResultSet.java new file mode 100644 index 0000000000..713d0663b3 --- /dev/null +++ b/source/java/org/alfresco/repo/search/EmptyResultSet.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; + +public class EmptyResultSet implements ResultSet +{ + + public EmptyResultSet() + { + super(); + } + + public Path[] getPropertyPaths() + { + return new Path[]{}; + } + + public int length() + { + return 0; + } + + public NodeRef getNodeRef(int n) + { + throw new UnsupportedOperationException(); + } + + public float getScore(int n) + { + throw new UnsupportedOperationException(); + } + + public Iterator iterator() + { + ArrayList dummy = new ArrayList(0); + return dummy.iterator(); + } + + public void close() + { + + } + + public ResultSetRow getRow(int i) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public List getNodeRefs() + { + return Collections.emptyList(); + } + + public List getChildAssocRefs() + { + return Collections.emptyList(); + } + + public ChildAssociationRef getChildAssocRef(int n) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } +} diff --git a/source/java/org/alfresco/repo/search/ISO9075.java b/source/java/org/alfresco/repo/search/ISO9075.java new file mode 100644 index 0000000000..fe1e8b8752 --- /dev/null +++ b/source/java/org/alfresco/repo/search/ISO9075.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.Collection; + +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +import com.sun.org.apache.xerces.internal.util.XMLChar; + +/** + * Support for the ISO 9075 encoding of XML element names. + * + * @author Andy Hind + */ +public class ISO9075 +{ + /* + * Mask for hex encoding + */ + private static final int MASK = (1 << 4) - 1; + + /* + * Digits used string encoding + */ + private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', + 'f' }; + + /** + * Private constructor + * + */ + private ISO9075() + { + super(); + } + + /** + * Encode a string according to ISO 9075 + * + * @param toEncode + * @return + */ + public static String encode(String toEncode) + { + if ((toEncode == null) || (toEncode.length() == 0)) + { + return toEncode; + } + else if (XMLChar.isValidName(toEncode) && (toEncode.indexOf("_x") == -1)) + { + return toEncode; + } + else + { + StringBuilder builder = new StringBuilder(toEncode.length()); + for (int i = 0; i < toEncode.length(); i++) + { + char c = toEncode.charAt(i); + // First requires special test + if (i == 0) + { + if (XMLChar.isNCNameStart(c)) + { + // The first character may be the _ at the start of an + // encoding pattern + if (matchesEncodedPattern(toEncode, i)) + { + // Encode the first _ + encode('_', builder); + } + else + { + // Just append + builder.append(c); + } + } + else + { + // Encode an invalid start character for an XML element + // name. + encode(c, builder); + } + } + else if (!XMLChar.isNCName(c)) + { + encode(c, builder); + } + else + { + if (matchesEncodedPattern(toEncode, i)) + { + // '_' must be encoded + encode('_', builder); + } + else + { + builder.append(c); + } + } + } + return builder.toString(); + } + + } + + private static boolean matchesEncodedPattern(String string, int position) + { + return (string.length() >= position + 6) + && (string.charAt(position) == '_') && (string.charAt(position + 1) == 'x') + && isHexChar(string.charAt(position + 2)) && isHexChar(string.charAt(position + 3)) + && isHexChar(string.charAt(position + 4)) && isHexChar(string.charAt(position + 5)) + && (string.charAt(position + 6) == '_'); + } + + private static boolean isHexChar(char c) + { + switch (c) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return true; + default: + return false; + } + } + + public static String decode(String toDecode) + { + if ((toDecode == null) || (toDecode.length() < 7) || (toDecode.indexOf("_x") < 0)) + { + return toDecode; + } + StringBuffer decoded = new StringBuffer(); + for (int i = 0, l = toDecode.length(); i < l; i++) + { + if (matchesEncodedPattern(toDecode, i)) + { + decoded.append(((char) Integer.parseInt(toDecode.substring(i + 2, i + 6), 16))); + i += 6; + } + else + { + decoded.append(toDecode.charAt(i)); + } + } + return decoded.toString(); + } + + private static void encode(char c, StringBuilder builder) + { + char[] buf = new char[] { '_', 'x', '0', '0', '0', '0', '_' }; + int charPos = 6; + do + { + buf[--charPos] = DIGITS[c & MASK]; + c >>>= 4; + } + while (c != 0); + builder.append(buf); + } + + public static String getXPathName(QName qName, NamespacePrefixResolver nspr) + { + + Collection prefixes = nspr.getPrefixes(qName.getNamespaceURI()); + if (prefixes.size() == 0) + { + throw new NamespaceException("A namespace prefix is not registered for uri " + qName.getNamespaceURI()); + } + String prefix = prefixes.iterator().next(); + if (prefix.equals(NamespaceService.DEFAULT_PREFIX)) + { + return ISO9075.encode(qName.getLocalName()); + } + else + { + return prefix + ":" + ISO9075.encode(qName.getLocalName()); + } + + } + + public static String getXPathName(QName qName) + { + + return "{" + qName.getNamespaceURI() + "}" + ISO9075.encode(qName.getLocalName()); + + } +} diff --git a/source/java/org/alfresco/repo/search/ISO9075Test.java b/source/java/org/alfresco/repo/search/ISO9075Test.java new file mode 100644 index 0000000000..cbc4480abe --- /dev/null +++ b/source/java/org/alfresco/repo/search/ISO9075Test.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import junit.framework.TestCase; + +public class ISO9075Test extends TestCase +{ + + public ISO9075Test() + { + super(); + } + + public ISO9075Test(String arg0) + { + super(arg0); + } + + public void testEncoding() + { + assertEquals("MyDocuments", ISO9075.encode("MyDocuments")); + assertEquals("My_Documents", ISO9075.encode("My_Documents")); + assertEquals("My_x0020_Documents", ISO9075.encode("My Documents")); + assertEquals("My_x0020Documents", ISO9075.encode("My_x0020Documents")); + assertEquals("My_x005f_x0020_Documents", ISO9075.encode("My_x0020_Documents")); + assertEquals("_x005f_x0020_Documents", ISO9075.encode("_x0020_Documents")); + assertEquals("_x0040__x005f_x0020_Documents", ISO9075.encode("@_x0020_Documents")); + assertEquals("Andy_x0027_s_x0020_Bits_x0020__x0026__x0020_Bobs_x0020__xabcd__x005c_", ISO9075 + .encode("Andy's Bits & Bobs \uabcd\\")); + assertEquals( + "_x0020__x0060__x00ac__x00a6__x0021__x0022__x00a3__x0024__x0025__x005e__x0026__x002a__x0028__x0029_-__x003d__x002b__x0009__x000a__x005c__x0000__x005b__x005d__x007b__x007d__x003b__x0027__x0023__x003a__x0040__x007e__x002c_._x002f__x003c__x003e__x003f__x005c__x007c_", + ISO9075.encode(" `¬¦!\"£$%^&*()-_=+\t\n\\\u0000[]{};'#:@~,./<>?\\|")); + assertEquals("\u0123_x4567_\u8900_xabcd__xefff__xT65A_", ISO9075.encode("\u0123\u4567\u8900\uabcd\uefff_xT65A_")); + + } + + public void testDeEncoding() + { + assertEquals("MyDocuments", ISO9075.decode("MyDocuments")); + assertEquals("My_Documents", ISO9075.decode("My_Documents")); + assertEquals("My Documents", ISO9075.decode("My_x0020_Documents")); + assertEquals("My_x0020Documents", ISO9075.decode("My_x0020Documents")); + assertEquals("My_x0020_Documents", ISO9075.decode("My_x005f_x0020_Documents")); + assertEquals("_x0020_Documents", ISO9075.decode("_x005f_x0020_Documents")); + assertEquals("@_x0020_Documents", ISO9075.decode("_x0040__x005f_x0020_Documents")); + assertEquals("Andy's Bits & Bobs \uabcd", ISO9075 + .decode("Andy_x0027_s_x0020_Bits_x0020__x0026__x0020_Bobs_x0020__xabcd_")); + assertEquals("Andy's Bits & Bobs \uabcd\\", ISO9075 + .decode("Andy_x0027_s_x0020_Bits_x0020__x0026__x0020_Bobs_x0020__xabcd__x005c_")); + assertEquals( + " `¬¦!\"£$%^&*()-_=+\t\n\\\u0000[]{};'#:@~,./<>?\\|", + ISO9075 + .decode("_x0020__x0060__x00ac__x00a6__x0021__x0022__x00a3__x0024__x0025__x005e__x0026__x002a__x0028__x0029_-__x003d__x002b__x0009__x000a__x005c__x0000__x005b__x005d__x007b__x007d__x003b__x0027__x0023__x003a__x0040__x007e__x002c_._x002f__x003c__x003e__x003f__x005c__x007c_")); + assertEquals("\u0123\u4567\u8900\uabcd\uefff_xT65A_", ISO9075.decode("\u0123_x4567_\u8900_xabcd__xefff__xT65A_")); + System.out.println(" `¬¦!\"£$%^&*()-_=+\t\n\\\u0000[]{};'#:@~,./<>?\\|\u0123\u4567\u8900\uabcd\uefff_xT65A_"); + } + +} diff --git a/source/java/org/alfresco/repo/search/Indexer.java b/source/java/org/alfresco/repo/search/Indexer.java new file mode 100644 index 0000000000..71a38fa741 --- /dev/null +++ b/source/java/org/alfresco/repo/search/Indexer.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * This interface abstracts how indexing is used from within the node service + * implementation. + * + * It has to optionally offer transactional integration For example, the lucene + * indexer + * + * @author andyh + */ + +public interface Indexer +{ + /** + * Create an index entry when a new node is created. A node is always + * created with a name in a given parent and so a relationship ref is + * required. + * + * @param relationshipRef + */ + public void createNode(ChildAssociationRef relationshipRef); + + /** + * Update an index entry due to property changes on a node. There are no + * strucural impications from such a change. + * + * @param nodeRef + */ + public void updateNode(NodeRef nodeRef); + + /** + * Delete a node entry from an index. This implies structural change. The + * node will be deleted from the index. This will also remove any remaining + * refernces to the node from the index. The index has no idea of the + * primary link. + * + * @param relationshipRef + */ + public void deleteNode(ChildAssociationRef relationshipRef); + + /** + * Create a refernce link between a parent and child. Implies only + * (potential) structural changes + * + * @param relationshipRef + */ + public void createChildRelationship(ChildAssociationRef relationshipRef); + + /** + * Alter the relationship between parent and child nodes in the index. + * + * This can be used for: + *

      + *
    1. rename, + *
    2. move, + *
    3. move and rename, + *
    4. replace + *
    + * + * This could be implemented as a delete and add but some implementations + * may be able to optimise this operation. + * + * @param relationshipBeforeRef + * @param relationshipAfterRef + */ + public void updateChildRelationship(ChildAssociationRef relationshipBeforeRef, ChildAssociationRef relationshipAfterRef); + + /** + * Delete a relationship between a parent and child. + * + * This will remove a structural route through the index. The index has no + * idea of reference and primary relationships and will happily remove the + * primary relationship before refernces which could remain. + * + * Use delete to ensure all strctural references are removed or call this + * sure you are doing an unlink (remove a hard link in the unix file system + * world). + * + * @param relationshipRef + */ + public void deleteChildRelationship(ChildAssociationRef relationshipRef); + + + +} diff --git a/source/java/org/alfresco/repo/search/IndexerAndSearcher.java b/source/java/org/alfresco/repo/search/IndexerAndSearcher.java new file mode 100644 index 0000000000..16f0094dd2 --- /dev/null +++ b/source/java/org/alfresco/repo/search/IndexerAndSearcher.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; + +/** + * Interface for Indexer and Searcher Factories to implement + * + * @author andyh + * + */ +public interface IndexerAndSearcher +{ + /** + * Get an indexer for a store + * + * @param storeRef + * @return + * @throws IndexerException + */ + public abstract Indexer getIndexer(StoreRef storeRef) throws IndexerException; + + /** + * Get a searcher for a store + * + * @param storeRef + * @param searchDelta - + * serach the in progress transaction as well as the main index + * (this is ignored for searches that do full text) + * @return + * @throws SearcherException + */ + public abstract SearchService getSearcher(StoreRef storeRef, boolean searchDelta) throws SearcherException; + + + /** + * Do any indexing that may be pending on behalf of the current transaction. + * + */ + public abstract void flush(); +} diff --git a/source/java/org/alfresco/repo/search/IndexerAndSearcherFactoryException.java b/source/java/org/alfresco/repo/search/IndexerAndSearcherFactoryException.java new file mode 100644 index 0000000000..5e99fcce03 --- /dev/null +++ b/source/java/org/alfresco/repo/search/IndexerAndSearcherFactoryException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +/** + * Factory related exception + * + * @author andyh + * + */ +public class IndexerAndSearcherFactoryException extends RuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = 3257850969667679025L; + + public IndexerAndSearcherFactoryException() + { + super(); + } + + public IndexerAndSearcherFactoryException(String message) + { + super(message); + } + + public IndexerAndSearcherFactoryException(String message, Throwable cause) + { + super(message, cause); + } + + public IndexerAndSearcherFactoryException(Throwable cause) + { + super(cause); + } + +} diff --git a/source/java/org/alfresco/repo/search/IndexerComponent.java b/source/java/org/alfresco/repo/search/IndexerComponent.java new file mode 100644 index 0000000000..097d4329e8 --- /dev/null +++ b/source/java/org/alfresco/repo/search/IndexerComponent.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Component API for indexing. Delegates to the real index retrieved from the + * {@link #indexerAndSearcherFactory} + * + * Transactional support is free. + * + * @see Indexer + * + * @author andyh + * + */ +public class IndexerComponent implements Indexer +{ + private IndexerAndSearcher indexerAndSearcherFactory; + + public void setIndexerAndSearcherFactory(IndexerAndSearcher indexerAndSearcherFactory) + { + this.indexerAndSearcherFactory = indexerAndSearcherFactory; + } + + public void createNode(ChildAssociationRef relationshipRef) + { + Indexer indexer = indexerAndSearcherFactory.getIndexer( + relationshipRef.getChildRef().getStoreRef()); + indexer.createNode(relationshipRef); + } + + public void updateNode(NodeRef nodeRef) + { + Indexer indexer = indexerAndSearcherFactory.getIndexer(nodeRef.getStoreRef()); + indexer.updateNode(nodeRef); + } + + public void deleteNode(ChildAssociationRef relationshipRef) + { + Indexer indexer = indexerAndSearcherFactory.getIndexer( + relationshipRef.getChildRef().getStoreRef()); + indexer.deleteNode(relationshipRef); + } + + public void createChildRelationship(ChildAssociationRef relationshipRef) + { + Indexer indexer = indexerAndSearcherFactory.getIndexer( + relationshipRef.getChildRef().getStoreRef()); + indexer.createChildRelationship(relationshipRef); + } + + public void updateChildRelationship(ChildAssociationRef relationshipBeforeRef, ChildAssociationRef relationshipAfterRef) + { + Indexer indexer = indexerAndSearcherFactory.getIndexer( + relationshipBeforeRef.getChildRef().getStoreRef()); + indexer.updateChildRelationship(relationshipBeforeRef, relationshipAfterRef); + } + + public void deleteChildRelationship(ChildAssociationRef relationshipRef) + { + Indexer indexer = indexerAndSearcherFactory.getIndexer( + relationshipRef.getChildRef().getStoreRef()); + indexer.deleteChildRelationship(relationshipRef); + } + +} diff --git a/source/java/org/alfresco/repo/search/IndexerException.java b/source/java/org/alfresco/repo/search/IndexerException.java new file mode 100644 index 0000000000..4eff149ce4 --- /dev/null +++ b/source/java/org/alfresco/repo/search/IndexerException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Indexer related exceptions + * + * @author andyh + */ +public class IndexerException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 3257286911646447666L; + + public IndexerException(String message) + { + super(message); + } + + public IndexerException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/source/java/org/alfresco/repo/search/NodeServiceXPath.java b/source/java/org/alfresco/repo/search/NodeServiceXPath.java new file mode 100644 index 0000000000..5540ad49a2 --- /dev/null +++ b/source/java/org/alfresco/repo/search/NodeServiceXPath.java @@ -0,0 +1,703 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jaxen.BaseXPath; +import org.jaxen.Context; +import org.jaxen.Function; +import org.jaxen.FunctionCallException; +import org.jaxen.FunctionContext; +import org.jaxen.JaxenException; +import org.jaxen.Navigator; +import org.jaxen.SimpleFunctionContext; +import org.jaxen.SimpleVariableContext; +import org.jaxen.function.BooleanFunction; +import org.jaxen.function.CeilingFunction; +import org.jaxen.function.ConcatFunction; +import org.jaxen.function.ContainsFunction; +import org.jaxen.function.CountFunction; +import org.jaxen.function.FalseFunction; +import org.jaxen.function.FloorFunction; +import org.jaxen.function.IdFunction; +import org.jaxen.function.LangFunction; +import org.jaxen.function.LastFunction; +import org.jaxen.function.LocalNameFunction; +import org.jaxen.function.NameFunction; +import org.jaxen.function.NamespaceUriFunction; +import org.jaxen.function.NormalizeSpaceFunction; +import org.jaxen.function.NotFunction; +import org.jaxen.function.NumberFunction; +import org.jaxen.function.PositionFunction; +import org.jaxen.function.RoundFunction; +import org.jaxen.function.StartsWithFunction; +import org.jaxen.function.StringFunction; +import org.jaxen.function.StringLengthFunction; +import org.jaxen.function.SubstringAfterFunction; +import org.jaxen.function.SubstringBeforeFunction; +import org.jaxen.function.SubstringFunction; +import org.jaxen.function.SumFunction; +import org.jaxen.function.TranslateFunction; +import org.jaxen.function.TrueFunction; +import org.jaxen.function.ext.EndsWithFunction; +import org.jaxen.function.ext.EvaluateFunction; +import org.jaxen.function.ext.LowerFunction; +import org.jaxen.function.ext.MatrixConcatFunction; +import org.jaxen.function.ext.UpperFunction; +import org.jaxen.function.xslt.DocumentFunction; + +/** + * Represents an xpath statement that resolves against a + * NodeService + * + * @author Andy Hind + */ +public class NodeServiceXPath extends BaseXPath +{ + private static final long serialVersionUID = 3834032441789592882L; + + private static String JCR_URI = "http://www.jcp.org/jcr/1.0"; + + private static Log logger = LogFactory.getLog(NodeServiceXPath.class); + + /** + * + * @param xpath + * the xpath statement + * @param documentNavigator + * the navigator that will allow the xpath to be resolved + * @param paramDefs + * parameters to resolve variables required by xpath + * @throws JaxenException + */ + public NodeServiceXPath(String xpath, DocumentNavigator documentNavigator, QueryParameterDefinition[] paramDefs) + throws JaxenException + { + super(xpath, documentNavigator); + + if (logger.isDebugEnabled()) + { + StringBuilder sb = new StringBuilder(); + sb.append("Created XPath: \n") + .append(" XPath: ").append(xpath).append("\n") + .append(" Parameters: \n"); + for (int i = 0; paramDefs != null && i < paramDefs.length; i++) + { + sb.append(" Parameter: \n") + .append(" name: ").append(paramDefs[i].getQName()).append("\n") + .append(" value: ").append(paramDefs[i].getDefault()).append("\n"); + } + logger.debug(sb.toString()); + } + + // Add support for parameters + if (paramDefs != null) + { + SimpleVariableContext svc = (SimpleVariableContext) this.getVariableContext(); + for (int i = 0; i < paramDefs.length; i++) + { + if (!paramDefs[i].hasDefaultValue()) + { + throw new AlfrescoRuntimeException("Parameter must have default value"); + } + Object value = null; + if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.BOOLEAN)) + { + value = Boolean.valueOf(paramDefs[i].getDefault()); + } + else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.DOUBLE)) + { + value = Double.valueOf(paramDefs[i].getDefault()); + } + else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.FLOAT)) + { + value = Float.valueOf(paramDefs[i].getDefault()); + } + else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.INT)) + { + value = Integer.valueOf(paramDefs[i].getDefault()); + } + else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.LONG)) + { + value = Long.valueOf(paramDefs[i].getDefault()); + } + else + { + value = paramDefs[i].getDefault(); + } + svc.setVariableValue(paramDefs[i].getQName().getNamespaceURI(), paramDefs[i].getQName().getLocalName(), + value); + } + } + + for (String prefix : documentNavigator.getNamespacePrefixResolver().getPrefixes()) + { + addNamespace(prefix, documentNavigator.getNamespacePrefixResolver().getNamespaceURI(prefix)); + } + } + + /** + * Jaxen has some magic with its IdentitySet, which means that we can get different results + * depending on whether we cache {@link ChildAssociationRef } instances or not. + *

    + * So, duplicates are eliminated here before the results are returned. + */ + @SuppressWarnings("unchecked") + @Override + public List selectNodes(Object arg0) throws JaxenException + { + if (logger.isDebugEnabled()) + { + logger.debug("Selecting using XPath: \n" + + " XPath: " + this + "\n" + + " starting at: " + arg0); + } + + List resultsWithDuplicates = super.selectNodes(arg0); + + Set set = new HashSet(resultsWithDuplicates); + + // now return as a list again + List results = resultsWithDuplicates; + results.clear(); + results.addAll(set); + + // done + return results; + } + + public static class FirstFunction implements Function + { + + public Object call(Context context, List args) throws FunctionCallException + { + if (args.size() == 0) + { + return evaluate(context); + } + + throw new FunctionCallException("first() requires no arguments."); + } + + public static Double evaluate(Context context) + { + return new Double(1); + } + } + + /** + * A boolean function to determine if a node type is a subtype of another + * type + */ + static class SubTypeOf implements Function + { + public Object call(Context context, List args) throws FunctionCallException + { + if (args.size() != 1) + { + throw new FunctionCallException("subtypeOf() requires one argument: subtypeOf(QName typeQName)"); + } + return evaluate(context.getNodeSet(), args.get(0), context.getNavigator()); + } + + public Object evaluate(List nodes, Object qnameObj, Navigator nav) + { + if (nodes.size() != 1) + { + return false; + } + // resolve the qname of the type we are checking for + String qnameStr = StringFunction.evaluate(qnameObj, nav); + if (qnameStr.equals("*")) + { + return true; + } + QName typeQName; + + if (qnameStr.startsWith("{")) + { + typeQName = QName.createQName(qnameStr); + } + else + { + typeQName = QName.createQName(qnameStr, ((DocumentNavigator) nav).getNamespacePrefixResolver()); + } + // resolve the noderef + NodeRef nodeRef = null; + if (nav.isElement(nodes.get(0))) + { + nodeRef = ((ChildAssociationRef) nodes.get(0)).getChildRef(); + } + else if (nav.isAttribute(nodes.get(0))) + { + nodeRef = ((DocumentNavigator.Property) nodes.get(0)).parent; + } + + DocumentNavigator dNav = (DocumentNavigator) nav; + boolean result = dNav.isSubtypeOf(nodeRef, typeQName); + return result; + } + } + + static class Deref implements Function + { + + public Object call(Context context, List args) throws FunctionCallException + { + if (args.size() == 2) + { + return evaluate(args.get(0), args.get(1), context.getNavigator()); + } + + throw new FunctionCallException("deref() requires two arguments."); + } + + public Object evaluate(Object attributeName, Object pattern, Navigator nav) + { + List answer = new ArrayList(); + String attributeValue = StringFunction.evaluate(attributeName, nav); + String patternValue = StringFunction.evaluate(pattern, nav); + + // TODO: Ignore the pattern for now + // Should do a type pattern test + if ((attributeValue != null) && (attributeValue.length() > 0)) + { + DocumentNavigator dNav = (DocumentNavigator) nav; + NodeRef nodeRef = new NodeRef(attributeValue); + if (patternValue.equals("*")) + { + answer.add(dNav.getNode(nodeRef)); + } + else + { + QNamePattern qNamePattern = new JCRPatternMatch(patternValue, dNav.getNamespacePrefixResolver()); + answer.addAll(dNav.getNode(nodeRef, qNamePattern)); + } + + } + return answer; + + } + } + + /** + * A boolean function to determine if a node property matches a pattern + * and/or the node text matches the pattern. + *

    + * The default is JSR170 compliant. The optional boolean allows searching + * only against the property value itself. + *

    + * The search is always case-insensitive. + * + * @author Derek Hulley + */ + static class Like implements Function + { + public Object call(Context context, List args) throws FunctionCallException + { + if (args.size() < 2 || args.size() > 3) + { + throw new FunctionCallException("like() usage: like(@attr, 'pattern' [, includeFTS]) \n" + + " - includeFTS can be 'true' or 'false' \n" + + " - search is case-insensitive"); + } + // default includeFTS to true + return evaluate(context.getNodeSet(), args.get(0), args.get(1), args.size() == 2 ? Boolean.toString(true) + : args.get(2), context.getNavigator()); + } + + public Object evaluate(List nodes, Object obj, Object patternObj, Object includeFtsObj, Navigator nav) + { + Object attribute = null; + if (obj instanceof List) + { + List list = (List) obj; + if (list.isEmpty()) + { + return false; + } + // do not recurse: only first list should unwrap + attribute = list.get(0); + } + if ((attribute == null) || !nav.isAttribute(attribute)) + { + return false; + } + if (nodes.size() != 1) + { + return false; + } + if (!nav.isElement(nodes.get(0))) + { + return false; + } + ChildAssociationRef car = (ChildAssociationRef) nodes.get(0); + String pattern = StringFunction.evaluate(patternObj, nav); + boolean includeFts = BooleanFunction.evaluate(includeFtsObj, nav); + QName qname = QName.createQName(nav.getAttributeNamespaceUri(attribute), ISO9075.decode(nav + .getAttributeName(attribute))); + + DocumentNavigator dNav = (DocumentNavigator) nav; + // JSR 170 includes full text matches + return dNav.like(car.getChildRef(), qname, pattern, includeFts); + + } + } + + static class Contains implements Function + { + + public Object call(Context context, List args) throws FunctionCallException + { + if (args.size() == 1) + { + return evaluate(context.getNodeSet(), args.get(0), context.getNavigator()); + } + + throw new FunctionCallException("contains() requires one argument."); + } + + public Object evaluate(List nodes, Object pattern, Navigator nav) + { + if (nodes.size() != 1) + { + return false; + } + QName qname = null; + NodeRef nodeRef = null; + if (nav.isElement(nodes.get(0))) + { + qname = null; // should use all attributes and full text index + nodeRef = ((ChildAssociationRef) nodes.get(0)).getChildRef(); + } + else if (nav.isAttribute(nodes.get(0))) + { + qname = QName.createQName(nav.getAttributeNamespaceUri(nodes.get(0)), ISO9075.decode(nav + .getAttributeName(nodes.get(0)))); + nodeRef = ((DocumentNavigator.Property) nodes.get(0)).parent; + } + + String patternValue = StringFunction.evaluate(pattern, nav); + DocumentNavigator dNav = (DocumentNavigator) nav; + + return dNav.contains(nodeRef, qname, patternValue, SearchParameters.OR); + + } + } + + static class JCRContains implements Function + { + + public Object call(Context context, List args) throws FunctionCallException + { + if (args.size() == 2) + { + if (context.getNavigator().isAttribute(context.getNodeSet().get(0))) + { + throw new FunctionCallException("jcr:contains() does not apply to an attribute context."); + } + return evaluate(context.getNodeSet(), args.get(0), args.get(1), context.getNavigator()); + } + + throw new FunctionCallException("contains() requires two argument."); + } + + public Object evaluate(List nodes, Object identifier, Object pattern, Navigator nav) + { + if (nodes.size() != 1) + { + return false; + } + + QName qname = null; + NodeRef nodeRef = null; + + Object target = identifier; + + if (identifier instanceof List) + { + List list = (List) identifier; + if (list.isEmpty()) + { + return false; + } + // do not recurse: only first list should unwrap + target = list.get(0); + } + + if (nav.isElement(target)) + { + qname = null; // should use all attributes and full text index + nodeRef = ((ChildAssociationRef) target).getChildRef(); + } + else if (nav.isAttribute(target)) + { + qname = QName.createQName(nav.getAttributeNamespaceUri(target), ISO9075.decode(nav.getAttributeName(target))); + nodeRef = ((DocumentNavigator.Property) target).parent; + } + + String patternValue = StringFunction.evaluate(pattern, nav); + DocumentNavigator dNav = (DocumentNavigator) nav; + + return dNav.contains(nodeRef, qname, patternValue, SearchParameters.AND); + + } + } + + static class Score implements Function + { + private Double one = new Double(1); + + public Object call(Context context, List args) throws FunctionCallException + { + return evaluate(context.getNodeSet(), context.getNavigator()); + } + + public Object evaluate(List nodes, Navigator nav) + { + return one; + + } + } + + protected FunctionContext createFunctionContext() + { + return XPathFunctionContext.getInstance(); + } + + public static class XPathFunctionContext extends SimpleFunctionContext + { + /** + * Singleton implementation. + */ + private static class Singleton + { + /** + * Singleton instance. + */ + private static XPathFunctionContext instance = new XPathFunctionContext(); + } + + /** + * Retrieve the singleton instance. + * + * @return the singleton instance + */ + public static FunctionContext getInstance() + { + return Singleton.instance; + } + + /** + * Construct. + * + *

    + * Construct with all core XPath functions registered. + *

    + */ + public XPathFunctionContext() + { + // XXX could this be a HotSpot???? + registerFunction("", // namespace URI + "boolean", new BooleanFunction()); + + registerFunction("", // namespace URI + "ceiling", new CeilingFunction()); + + registerFunction("", // namespace URI + "concat", new ConcatFunction()); + + registerFunction("", // namespace URI + "contains", new ContainsFunction()); + + registerFunction("", // namespace URI + "count", new CountFunction()); + + registerFunction("", // namespace URI + "document", new DocumentFunction()); + + registerFunction("", // namespace URI + "false", new FalseFunction()); + + registerFunction("", // namespace URI + "floor", new FloorFunction()); + + registerFunction("", // namespace URI + "id", new IdFunction()); + + registerFunction("", // namespace URI + "lang", new LangFunction()); + + registerFunction("", // namespace URI + "last", new LastFunction()); + + registerFunction("", // namespace URI + "local-name", new LocalNameFunction()); + + registerFunction("", // namespace URI + "name", new NameFunction()); + + registerFunction("", // namespace URI + "namespace-uri", new NamespaceUriFunction()); + + registerFunction("", // namespace URI + "normalize-space", new NormalizeSpaceFunction()); + + registerFunction("", // namespace URI + "not", new NotFunction()); + + registerFunction("", // namespace URI + "number", new NumberFunction()); + + registerFunction("", // namespace URI + "position", new PositionFunction()); + + registerFunction("", // namespace URI + "round", new RoundFunction()); + + registerFunction("", // namespace URI + "starts-with", new StartsWithFunction()); + + registerFunction("", // namespace URI + "string", new StringFunction()); + + registerFunction("", // namespace URI + "string-length", new StringLengthFunction()); + + registerFunction("", // namespace URI + "substring-after", new SubstringAfterFunction()); + + registerFunction("", // namespace URI + "substring-before", new SubstringBeforeFunction()); + + registerFunction("", // namespace URI + "substring", new SubstringFunction()); + + registerFunction("", // namespace URI + "sum", new SumFunction()); + + registerFunction("", // namespace URI + "true", new TrueFunction()); + + registerFunction("", // namespace URI + "translate", new TranslateFunction()); + + // register extension functions + // extension functions should go into a namespace, but which one? + // for now, keep them in default namespace to not break any code + + registerFunction("", // namespace URI + "matrix-concat", new MatrixConcatFunction()); + + registerFunction("", // namespace URI + "evaluate", new EvaluateFunction()); + + registerFunction("", // namespace URI + "lower-case", new LowerFunction()); + + registerFunction("", // namespace URI + "upper-case", new UpperFunction()); + + registerFunction("", // namespace URI + "ends-with", new EndsWithFunction()); + + registerFunction("", "subtypeOf", new SubTypeOf()); + registerFunction("", "deref", new Deref()); + registerFunction("", "like", new Like()); + registerFunction("", "contains", new Contains()); + + registerFunction("", "first", new FirstFunction()); + + // 170 functions + + registerFunction(JCR_URI, "like", new Like()); + registerFunction(JCR_URI, "score", new Score()); + registerFunction(JCR_URI, "contains", new JCRContains()); + registerFunction(JCR_URI, "deref", new Deref()); + + } + } + + public static class JCRPatternMatch implements QNamePattern + { + private List searches = new ArrayList(); + + private NamespacePrefixResolver resolver; + + /** + * Construct + * + * @param pattern + * JCR Pattern + * @param resolver + * Namespace Prefix Resolver + */ + public JCRPatternMatch(String pattern, NamespacePrefixResolver resolver) + { + // TODO: Check for valid pattern + + // Convert to regular expression + String regexPattern = pattern.replaceAll("\\*", ".*"); + + // Split into independent search strings + StringTokenizer tokenizer = new StringTokenizer(regexPattern, "|", false); + while (tokenizer.hasMoreTokens()) + { + String disjunct = tokenizer.nextToken().trim(); + this.searches.add(disjunct); + } + + this.resolver = resolver; + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.service.namespace.QNamePattern#isMatch(org.alfresco.service.namespace.QName) + */ + public boolean isMatch(QName qname) + { + String prefixedName = qname.toPrefixString(resolver); + for (String search : searches) + { + if (prefixedName.matches(search)) + { + return true; + } + } + return false; + } + + } + +} diff --git a/source/java/org/alfresco/repo/search/QueryCollection.java b/source/java/org/alfresco/repo/search/QueryCollection.java new file mode 100644 index 0000000000..8a135cf6da --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryCollection.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + +public interface QueryCollection +{ + /** + * The name of the query collection + * + * @return + */ + public String getName(); + + /** + * Does this collection contain a query for the given QName? + * @param qName + * @return + */ + public boolean containsQueryDefinition(QName qName); + + /** + * Get a query definition by QName. + * @param qName + * @return + */ + public CannedQueryDef getQueryDefinition(QName qName); + + /** + * Does this collection contain a query for the given QName? + * @param qName + * @return + */ + public boolean containsParameterDefinition(QName qName); + + /** + * Get a query definition by QName. + * @param qName + * @return + */ + public QueryParameterDefinition getParameterDefinition(QName qName); + + /** + * Return the mechanism that this query definition uses to map namespace prefixes to URIs. + * A query may use a predefined set of prefixes for known URIs. + * I would be unwise to rely on the defaults. + * + * @return + */ + public NamespacePrefixResolver getNamespacePrefixResolver(); + +} diff --git a/source/java/org/alfresco/repo/search/QueryCollectionImpl.java b/source/java/org/alfresco/repo/search/QueryCollectionImpl.java new file mode 100644 index 0000000000..a5c48974b0 --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryCollectionImpl.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.dom4j.Element; +import org.dom4j.Namespace; + +public class QueryCollectionImpl implements QueryCollection +{ + private static final org.dom4j.QName ELEMENT_QNAME = new org.dom4j.QName("query-register", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName NAME = new org.dom4j.QName("name", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName NAMESPACES = new org.dom4j.QName("namespaces", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName NAMESPACE = new org.dom4j.QName("namespace", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName PREFIX = new org.dom4j.QName("prefix", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName URI = new org.dom4j.QName("uri", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private String name; + + private Map parameters = new HashMap(); + + private Map queries = new HashMap(); + + NamespacePrefixResolver namespacePrefixResolver; + + public QueryCollectionImpl(String name, Map parameters, NamespacePrefixResolver namespacePrefixResolver) + { + super(); + this.name = name; + this.parameters = parameters; + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public String getName() + { + return name; + } + + public boolean containsQueryDefinition(QName qName) + { + return queries.containsKey(qName); + } + + private void addQueryDefinition(CannedQueryDef queryDefinition) + { + queries.put(queryDefinition.getQname(), queryDefinition); + } + + public CannedQueryDef getQueryDefinition(QName qName) + { + return queries.get(qName); + } + + public boolean containsParameterDefinition(QName qName) + { + return parameters.containsKey(qName); + } + + public QueryParameterDefinition getParameterDefinition(QName qName) + { + return parameters.get(qName); + } + + public NamespacePrefixResolver getNamespacePrefixResolver() + { + return namespacePrefixResolver; + } + + + public static QueryCollection createQueryCollection(Element element, DictionaryService dictionaryService, NamespacePrefixResolver nspr) + { + DynamicNamespacePrefixResolver dnpr = new DynamicNamespacePrefixResolver(nspr); + if (element.getName().equals(ELEMENT_QNAME.getName())) + { + String name = null; + Element nameElement = element.element(NAME.getName()); + if(nameElement != null) + { + name = nameElement.getText(); + } + + Element nameSpaces = element.element(NAMESPACES.getName()); + if(nameSpaces != null) + { + List ns = nameSpaces.elements(NAMESPACE.getName()); + for(Iterator it = ns.iterator(); it.hasNext(); /**/) + { + Element nsElement = (Element)it.next(); + Element prefixElement = nsElement.element(PREFIX.getName()); + Element uriElement = nsElement.element(URI.getName()); + if((prefixElement != null) && (nsElement != null)) + { + dnpr.registerNamespace(prefixElement.getText(), uriElement.getText()); + } + } + } + + // Do property definitions so they are available to query defintions + + Map parameters = new HashMap(); + List list = element.elements(QueryParameterDefImpl.getElementQName().getName()); + for(Iterator it = list.iterator(); it.hasNext(); /**/) + { + Element defElement = (Element) it.next(); + QueryParameterDefinition paramDef = QueryParameterDefImpl.createParameterDefinition(defElement, dictionaryService, nspr); + parameters.put(paramDef.getQName(), paramDef); + } + + QueryCollectionImpl collection = new QueryCollectionImpl(name, parameters, dnpr); + + list = element.elements(CannedQueryDefImpl.getElementQName().getName()); + for(Iterator it = list.iterator(); it.hasNext(); /**/) + { + Element defElement = (Element) it.next(); + CannedQueryDefImpl queryDef = CannedQueryDefImpl.createCannedQuery(defElement, dictionaryService, collection, nspr); + collection.addQueryDefinition(queryDef); + } + + return collection; + } + else + { + return null; + } + } +} diff --git a/source/java/org/alfresco/repo/search/QueryParameterDefImpl.java b/source/java/org/alfresco/repo/search/QueryParameterDefImpl.java new file mode 100644 index 0000000000..3804f55583 --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryParameterDefImpl.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.dom4j.Element; +import org.dom4j.Namespace; + +public class QueryParameterDefImpl implements QueryParameterDefinition +{ + + private static final org.dom4j.QName ELEMENT_QNAME = new org.dom4j.QName("parameter-definition", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName DEF_QNAME = new org.dom4j.QName("qname", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName PROPERTY_QNAME = new org.dom4j.QName("property", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName PROPERTY_TYPE_QNAME = new org.dom4j.QName("type", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName DEFAULT_VALUE = new org.dom4j.QName("default-value", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + + private QName qName; + + private PropertyDefinition propertyDefintion; + + private DataTypeDefinition dataTypeDefintion; + + private boolean hasDefaultValue; + + private String defaultValue; + + public QueryParameterDefImpl(QName qName, PropertyDefinition propertyDefinition, boolean hasDefaultValue, String defaultValue) + { + this(qName, hasDefaultValue, defaultValue); + this.propertyDefintion = propertyDefinition; + this.dataTypeDefintion = propertyDefinition.getDataType(); + } + + private QueryParameterDefImpl(QName qName, boolean hasDefaultValue, String defaultValue) + { + super(); + this.qName = qName; + this.hasDefaultValue = hasDefaultValue; + this.defaultValue = defaultValue; + } + + public QueryParameterDefImpl(QName qName, DataTypeDefinition dataTypeDefintion, boolean hasDefaultValue, String defaultValue) + { + this(qName, hasDefaultValue, defaultValue); + this.propertyDefintion = null; + this.dataTypeDefintion = dataTypeDefintion; + } + + public QName getQName() + { + return qName; + } + + public PropertyDefinition getPropertyDefinition() + { + return propertyDefintion; + } + + public DataTypeDefinition getDataTypeDefinition() + { + return dataTypeDefintion; + } + + public static QueryParameterDefinition createParameterDefinition(Element element, DictionaryService dictionaryService, NamespacePrefixResolver nspr) + { + + if (element.getQName().getName().equals(ELEMENT_QNAME.getName())) + { + QName qName = null; + Element qNameElement = element.element(DEF_QNAME.getName()); + if (qNameElement != null) + { + qName = QName.createQName(qNameElement.getText(), nspr); + } + + PropertyDefinition propDef = null; + Element propDefElement = element.element(PROPERTY_QNAME.getName()); + if (propDefElement != null) + { + propDef = dictionaryService.getProperty(QName.createQName(propDefElement.getText(), nspr)); + } + + DataTypeDefinition typeDef = null; + Element typeDefElement = element.element(PROPERTY_TYPE_QNAME.getName()); + if (typeDefElement != null) + { + typeDef = dictionaryService.getDataType(QName.createQName(typeDefElement.getText(), nspr)); + } + + boolean hasDefault = false; + String defaultValue = null; + Element defaultValueElement = element.element(DEFAULT_VALUE.getName()); + if(defaultValueElement != null) + { + hasDefault = true; + defaultValue = defaultValueElement.getText(); + } + + if (propDef != null) + { + return new QueryParameterDefImpl(qName, propDef, hasDefault, defaultValue); + } + else + { + return new QueryParameterDefImpl(qName, typeDef, hasDefault, defaultValue); + } + } + else + { + return null; + } + } + + public static org.dom4j.QName getElementQName() + { + return ELEMENT_QNAME; + } + + public QueryParameterDefinition getQueryParameterDefinition() + { + return this; + } + + /** + * There may be a default value which is null ie the empty + * string or no entry at all for no default + * value + */ + public String getDefault() + { + return defaultValue; + } + + public boolean hasDefaultValue() + { + return hasDefaultValue; + } + +} diff --git a/source/java/org/alfresco/repo/search/QueryParameterRefImpl.java b/source/java/org/alfresco/repo/search/QueryParameterRefImpl.java new file mode 100644 index 0000000000..8532dbbad7 --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryParameterRefImpl.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.search.NamedQueryParameterDefinition; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.dom4j.Element; +import org.dom4j.Namespace; + +public class QueryParameterRefImpl implements NamedQueryParameterDefinition +{ + + private static final org.dom4j.QName ELEMENT_QNAME = new org.dom4j.QName("parameter-ref", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private static final org.dom4j.QName DEF_QNAME = new org.dom4j.QName("qname", new Namespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI)); + + private QName qName; + + private QueryCollection container; + + public QueryParameterRefImpl(QName qName, QueryCollection container) + { + super(); + this.qName = qName; + this.container = container; + } + + public QName getQName() + { + return qName; + } + + public static NamedQueryParameterDefinition createParameterReference(Element element, DictionaryService dictionaryService, QueryCollection container) + { + + if (element.getQName().getName().equals(ELEMENT_QNAME.getName())) + { + QName qName = null; + Element qNameElement = element.element(DEF_QNAME.getName()); + if(qNameElement != null) + { + qName = QName.createQName(qNameElement.getText(), container.getNamespacePrefixResolver()); + } + + return new QueryParameterRefImpl(qName, container); + } + else + { + return null; + } + } + + public static org.dom4j.QName getElementQName() + { + return ELEMENT_QNAME; + } + + public QueryParameterDefinition getQueryParameterDefinition() + { + return container.getParameterDefinition(getQName()); + } + +} diff --git a/source/java/org/alfresco/repo/search/QueryRegisterComponent.java b/source/java/org/alfresco/repo/search/QueryRegisterComponent.java new file mode 100644 index 0000000000..1bf09120c0 --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryRegisterComponent.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.QName; + +public interface QueryRegisterComponent +{ + /** + * Get a query defintion by Qname + * + * @param qName + * @return + */ + public CannedQueryDef getQueryDefinition(QName qName); + + /** + * Get the name of the collection containing a query + * + * @param qName + * @return + */ + public String getCollectionNameforQueryDefinition(QName qName); + + /** + * Get a parameter definition + * + * @param qName + * @return + */ + public QueryParameterDefinition getParameterDefinition(QName qName); + + /** + * Get the name of the collection containing a parameter definition + * + * @param qName + * @return + */ + public String getCollectionNameforParameterDefinition(QName qName); + + + /** + * Get a query collection by name + * + * @param name + * @return + */ + public QueryCollection getQueryCollection(String name); + + + /** + * Load a query collection + * + * @param location + */ + public void loadQueryCollection(String location); +} diff --git a/source/java/org/alfresco/repo/search/QueryRegisterComponentImpl.java b/source/java/org/alfresco/repo/search/QueryRegisterComponentImpl.java new file mode 100644 index 0000000000..d766fe93ce --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryRegisterComponentImpl.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.io.SAXReader; + +public class QueryRegisterComponentImpl implements QueryRegisterComponent +{ + private DictionaryService dictionaryService; + + private NamespacePrefixResolver namespaceService; + + private List initCollections = null; + + private Map collections = new HashMap(); + + public QueryRegisterComponentImpl() + { + super(); + } + + private synchronized void loadCollectionsOnDemand() + { + if(initCollections != null) + { + for(String location: initCollections) + { + loadQueryCollection(location); + } + } + initCollections = null; + } + + public CannedQueryDef getQueryDefinition(QName qName) + { + loadCollectionsOnDemand(); + for(String key: collections.keySet()) + { + QueryCollection collection = collections.get(key); + CannedQueryDef def = collection.getQueryDefinition(qName); + if(def != null) + { + return def; + } + } + return null; + } + + public String getCollectionNameforQueryDefinition(QName qName) + { + loadCollectionsOnDemand(); + for(String key: collections.keySet()) + { + QueryCollection collection = collections.get(key); + if(collection.containsQueryDefinition(qName)) + { + return key; + } + } + return null; + } + + public QueryParameterDefinition getParameterDefinition(QName qName) + { + loadCollectionsOnDemand(); + for(String key: collections.keySet()) + { + QueryCollection collection = collections.get(key); + QueryParameterDefinition def = collection.getParameterDefinition(qName); + if(def != null) + { + return def; + } + } + return null; + } + + public String getCollectionNameforParameterDefinition(QName qName) + { + loadCollectionsOnDemand(); + for(String key: collections.keySet()) + { + QueryCollection collection = collections.get(key); + if(collection.containsParameterDefinition(qName)) + { + return key; + } + } + return null; + } + + public QueryCollection getQueryCollection(String location) + { + loadCollectionsOnDemand(); + return collections.get(location); + } + + public void loadQueryCollection(String location) + { + try + { + InputStream is = this.getClass().getClassLoader().getResourceAsStream(location); + SAXReader reader = new SAXReader(); + Document document = reader.read(is); + is.close(); + QueryCollection collection = QueryCollectionImpl.createQueryCollection(document.getRootElement(), dictionaryService, namespaceService); + collections.put(location, collection); + } + catch (DocumentException de) + { + throw new AlfrescoRuntimeException("Error reading XML", de); + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("IO Error reading XML", e); + } + } + + public void setCollections(List collections) + { + this.initCollections = collections; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNamespaceService(NamespacePrefixResolver namespaceService) + { + this.namespaceService = namespaceService; + } +} diff --git a/source/java/org/alfresco/repo/search/QueryRegisterComponentTest.java b/source/java/org/alfresco/repo/search/QueryRegisterComponentTest.java new file mode 100644 index 0000000000..79ad8bee6e --- /dev/null +++ b/source/java/org/alfresco/repo/search/QueryRegisterComponentTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class QueryRegisterComponentTest extends TestCase +{ + static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + + public QueryRegisterComponentTest() + { + super(); + } + + public QueryRegisterComponentTest(String arg0) + { + super(arg0); + } + + public void setUp() + { + + dictionaryService = (DictionaryService) ctx.getBean("dictionaryService"); + namespaceService = (NamespaceService) ctx.getBean("namespaceService"); + + } + + public void testLoad() + { + QueryRegisterComponentImpl qr = new QueryRegisterComponentImpl(); + qr.setNamespaceService(namespaceService); + qr.setDictionaryService(dictionaryService); + qr.loadQueryCollection("testQueryRegister.xml"); + + assertNotNull(qr.getQueryDefinition(QName.createQName("alf", "query1", namespaceService))); + assertEquals("lucene", qr.getQueryDefinition(QName.createQName("alf", "query1", namespaceService)).getLanguage()); + assertEquals("http://www.trees.tulip/barking/woof", qr.getQueryDefinition(QName.createQName("alf", "query1", namespaceService)).getNamespacePrefixResolver().getNamespaceURI("tulip")); + assertEquals("+QNAME:$alf:query-parameter-name", qr.getQueryDefinition(QName.createQName("alf", "query1", namespaceService)).getQuery()); + assertEquals(2, qr.getQueryDefinition(QName.createQName("alf", "query1", namespaceService)).getQueryParameterDefs().size()); + } + +} diff --git a/source/java/org/alfresco/repo/search/ResultSetRowIterator.java b/source/java/org/alfresco/repo/search/ResultSetRowIterator.java new file mode 100644 index 0000000000..ad43129646 --- /dev/null +++ b/source/java/org/alfresco/repo/search/ResultSetRowIterator.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.util.ListIterator; + +import org.alfresco.service.cmr.search.ResultSetRow; + +/** + * A typed ListIterator over Collections containing ResultSetRow elements + * + * @author andyh + * + */ +public interface ResultSetRowIterator extends ListIterator +{ + +} diff --git a/source/java/org/alfresco/repo/search/SearcherComponent.java b/source/java/org/alfresco/repo/search/SearcherComponent.java new file mode 100644 index 0000000000..581b309cf4 --- /dev/null +++ b/source/java/org/alfresco/repo/search/SearcherComponent.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.XPathException; +import org.alfresco.service.cmr.search.QueryParameter; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + +/** + * Component API for searching. Delegates to the real {@link org.alfresco.service.cmr.search.SearchService searcher} + * from the {@link #indexerAndSearcherFactory}. + * + * Transactional support is free. + * + * @author andyh + * + */ +public class SearcherComponent extends AbstractSearcherComponent +{ + private IndexerAndSearcher indexerAndSearcherFactory; + + public void setIndexerAndSearcherFactory(IndexerAndSearcher indexerAndSearcherFactory) + { + this.indexerAndSearcherFactory = indexerAndSearcherFactory; + } + + public ResultSet query(StoreRef store, + String language, + String query, + Path[] queryOptions, + QueryParameterDefinition[] queryParameterDefinitions) + { + SearchService searcher = indexerAndSearcherFactory.getSearcher(store, true); + return searcher.query(store, language, query, queryOptions, queryParameterDefinitions); + } + + public ResultSet query(StoreRef store, QName queryId, QueryParameter[] queryParameters) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public ResultSet query(SearchParameters searchParameters) + { + if(searchParameters.getStores().size() != 1) + { + throw new IllegalStateException("Only one store can be searched at present"); + } + StoreRef storeRef = searchParameters.getStores().get(0); + SearchService searcher = indexerAndSearcherFactory.getSearcher(storeRef, !searchParameters.excludeDataInTheCurrentTransaction()); + return searcher.query(searchParameters); + } + + public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern) throws InvalidNodeRefException + { + return contains(nodeRef, propertyQName, googleLikePattern, SearchParameters.Operator.OR); + } + + public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern, SearchParameters.Operator defaultOperator) throws InvalidNodeRefException + { + SearchService searcher = indexerAndSearcherFactory.getSearcher(nodeRef.getStoreRef(), true); + return searcher.contains(nodeRef, propertyQName, googleLikePattern); + } + + public boolean like(NodeRef nodeRef, QName propertyQName, String sqlLikePattern, boolean includeFTS) throws InvalidNodeRefException + { + SearchService searcher = indexerAndSearcherFactory.getSearcher(nodeRef.getStoreRef(), true); + return searcher.like(nodeRef, propertyQName, sqlLikePattern, includeFTS); + } + + public List selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks, String language) throws InvalidNodeRefException, XPathException + { + SearchService searcher = indexerAndSearcherFactory.getSearcher(contextNodeRef.getStoreRef(), true); + return searcher.selectNodes(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks, language); + } + + public List selectProperties(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks, String language) throws InvalidNodeRefException, XPathException + { + SearchService searcher = indexerAndSearcherFactory.getSearcher(contextNodeRef.getStoreRef(), true); + return searcher.selectProperties(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks, language); + } +} diff --git a/source/java/org/alfresco/repo/search/SearcherComponentTest.java b/source/java/org/alfresco/repo/search/SearcherComponentTest.java new file mode 100644 index 0000000000..1986893c8f --- /dev/null +++ b/source/java/org/alfresco/repo/search/SearcherComponentTest.java @@ -0,0 +1,1136 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @see org.alfresco.repo.search.SearcherComponent + * + * @author Derek Hulley + */ +public class SearcherComponentTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private static String COMPLEX_LOCAL_NAME = " `¬¦!\"£$%^&*()-_=+\t\n\\\u0000[]{};'#:@~,./<>?\\|\u0123\u4567\u8900\uabcd\uefff_xT65A_"; + + private ServiceRegistry serviceRegistry; + private TransactionService transactionService; + private DictionaryService dictionaryService; + private SearcherComponent searcher; + private NodeService nodeService; + private AuthenticationComponent authenticationComponent; + + private NodeRef rootNodeRef; + private UserTransaction txn; + + public void setUp() throws Exception + { + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + dictionaryService = BaseNodeServiceTest.loadModel(ctx); + nodeService = serviceRegistry.getNodeService(); + + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + this.authenticationComponent.setSystemUserAsCurrentUser(); + + // get the indexer and searcher factory + IndexerAndSearcher indexerAndSearcher = (IndexerAndSearcher) ctx.getBean("indexerAndSearcherFactory"); + searcher = new SearcherComponent(); + searcher.setIndexerAndSearcherFactory(indexerAndSearcher); + // create a test workspace + StoreRef storeRef = nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + getName() + "_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + // begin a transaction + txn = transactionService.getUserTransaction(); + txn.begin(); + } + + public void tearDown() throws Exception + { + if (txn.getStatus() == Status.STATUS_ACTIVE) + { + txn.rollback(); + } + authenticationComponent.clearCurrentSecurityContext(); + super.tearDown(); + } + + public void testNodeXPath() throws Exception + { + + Map assocRefs = BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + + Map properties = new HashMap(); + properties.put(QName.createQName(BaseNodeServiceTest.NAMESPACE, COMPLEX_LOCAL_NAME), "monkey"); + QName qnamerequiringescaping = QName.createQName(BaseNodeServiceTest.NAMESPACE, COMPLEX_LOCAL_NAME); + nodeService.createNode(rootNodeRef, BaseNodeServiceTest.ASSOC_TYPE_QNAME_TEST_CHILDREN, qnamerequiringescaping, ContentModel.TYPE_CONTAINER); + QName qname = QName.createQName(BaseNodeServiceTest.NAMESPACE, "n2_p_n4"); + + + NodeServiceXPath xpath; + String xpathStr; + QueryParameterDefImpl paramDef; + List list; + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + // create the document navigator + DocumentNavigator documentNavigator = new DocumentNavigator( + dictionaryService, + nodeService, + searcher, + namespacePrefixResolver, + false, false); + + xpath = new NodeServiceXPath("//.[@test:animal='monkey']", documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("*", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(3, list.size()); + + xpath = new NodeServiceXPath("*/*", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(4, list.size()); + + xpath = new NodeServiceXPath("*/*/*", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(3, list.size()); + + xpath = new NodeServiceXPath("*/*/*/*", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(2, list.size()); + + xpath = new NodeServiceXPath("*/*/*/*/..", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(2, list.size()); + + xpath = new NodeServiceXPath("*//.", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(12, list.size()); + + xpathStr = "test:root_p_n1"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpathStr = "*//.[@test:animal]"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpathStr = "*//.[@test:animal='monkey']"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpathStr = "//.[@test:animal='monkey']"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + paramDef = new QueryParameterDefImpl( + QName.createQName("test:test", namespacePrefixResolver), + dictionaryService.getDataType(DataTypeDefinition.TEXT), + true, + "monkey"); + xpathStr = "//.[@test:animal=$test:test]"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, new QueryParameterDefinition[]{paramDef}); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath(".", documentNavigator, null); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/test:"+ISO9075.encode(COMPLEX_LOCAL_NAME), documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("//test:"+ISO9075.encode(COMPLEX_LOCAL_NAME), documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("..", documentNavigator, null); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + + // follow all parent links now + documentNavigator.setFollowAllParentLinks(true); + + xpath = new NodeServiceXPath("..", documentNavigator, null); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(2, list.size()); + + xpathStr = "//@test:animal"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + assertTrue(list.get(0) instanceof DocumentNavigator.Property); + + xpathStr = "//@test:reference"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + + // stop following parent links + documentNavigator.setFollowAllParentLinks(false); + + xpathStr = "deref(/test:root_p_n1/test:n1_p_n3/@test:reference, '*')"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + + xpathStr = "deref(/test:root_p_n1/test:n1_p_n3/@test:reference, 'test:root_p_n1')"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(0, list.size()); + + xpathStr = "deref(/test:root_p_n1/test:n1_p_n3/@test:reference, 'test:root_p_n2')"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + + + // test 'subtypeOf' function + paramDef = new QueryParameterDefImpl( + QName.createQName("test:type", namespacePrefixResolver), + dictionaryService.getDataType(DataTypeDefinition.QNAME), + true, + BaseNodeServiceTest.TYPE_QNAME_TEST_CONTENT.toPrefixString(namespacePrefixResolver)); + xpathStr = "//.[subtypeOf($test:type)]"; + xpath = new NodeServiceXPath(xpathStr, documentNavigator, new QueryParameterDefinition[]{paramDef}); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(2, list.size()); // 2 distinct paths to node n8, which is of type content + + xpath = new NodeServiceXPath("/", documentNavigator, null); + xpath.addNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + list = xpath.selectNodes(assocRefs.get(qname)); + assertEquals(1, list.size()); + } + + + public void testSelectAPI() throws Exception + { + Map assocRefs = BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + NodeRef n6Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n3_p_n6")).getChildRef(); + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + + List answer = searcher.selectNodes(rootNodeRef, "/test:root_p_n1/test:n1_p_n3/*", null, namespacePrefixResolver, false); + assertEquals(1, answer.size()); + assertTrue(answer.contains(n6Ref)); + + //List + answer = searcher.selectNodes(rootNodeRef, "*", null, namespacePrefixResolver, false); + assertEquals(2, answer.size()); + + List attributes = searcher.selectProperties(rootNodeRef, "//@test:animal", null, namespacePrefixResolver, false); + assertEquals(1, attributes.size()); + } + + /** + * Tests the like and contains functions (FTS functions) within a currently executing + * transaction + */ + public void testLikeAndContains() throws Exception + { + Map assocRefs = BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + + + Map properties = new HashMap(); + properties.put(QName.createQName(BaseNodeServiceTest.NAMESPACE, COMPLEX_LOCAL_NAME), "monkey"); + QName qnamerequiringescaping = QName.createQName(BaseNodeServiceTest.NAMESPACE, COMPLEX_LOCAL_NAME); + nodeService.createNode(rootNodeRef, BaseNodeServiceTest.ASSOC_TYPE_QNAME_TEST_CHILDREN, qnamerequiringescaping, ContentModel.TYPE_CONTAINER, properties); + + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + + List answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:animal, 'm__k%', false)]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:"+ISO9075.encode(COMPLEX_LOCAL_NAME)+", 'm__k%', false)]", + null, + namespacePrefixResolver, false); +// assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:animal, 'M__K%', false)]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:"+ISO9075.encode(COMPLEX_LOCAL_NAME)+", 'M__K%', false)]", + null, + namespacePrefixResolver, false); +// assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:UPPERANIMAL, 'm__k%', false)]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:UPPERANIMAL, 'M__K%', false)]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[like(@test:UPPERANIMAL, 'M__K%', true)]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes(rootNodeRef, "//*[contains('monkey')]", null, namespacePrefixResolver, false); + assertEquals(2, answer.size()); + + answer = searcher.selectNodes(rootNodeRef, "//*[contains('MONKEY')]", null, namespacePrefixResolver, false); + assertEquals(2, answer.size()); + + answer = searcher.selectNodes(rootNodeRef, "//*[contains(lower-case('MONKEY'))]", null, namespacePrefixResolver, false); + assertEquals(2, answer.size()); + + // select the monkey node in the second level + QueryParameterDefinition[] paramDefs = new QueryParameterDefinition[2]; + paramDefs[0] = new QueryParameterDefImpl( + QName.createQName("test:animal", namespacePrefixResolver), + dictionaryService.getDataType(DataTypeDefinition.TEXT), + true, + "monkey%"); + paramDefs[1] = new QueryParameterDefImpl( + QName.createQName("test:type", namespacePrefixResolver), + dictionaryService.getDataType(DataTypeDefinition.TEXT), + true, + BaseNodeServiceTest.TYPE_QNAME_TEST_CONTENT.toString()); + answer = searcher.selectNodes( + rootNodeRef, + "./*/*[like(@test:animal, $test:animal, false) or subtypeOf($test:type)]", + paramDefs, + namespacePrefixResolver, + false); + assertEquals(1, answer.size()); + + // select the monkey node again, but use the first level as the starting poing + NodeRef n1Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "root_p_n1")).getChildRef(); + NodeRef n3Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n1_p_n3")).getChildRef(); + // first time go too deep + answer = searcher.selectNodes( + n1Ref, + "./*/*[like(@test:animal, $test:animal, false) or subtypeOf($test:type)]", + paramDefs, + namespacePrefixResolver, + false); + assertEquals(0, answer.size()); + // second time get it right + answer = searcher.selectNodes( + n1Ref, + "./*[like(@test:animal, $test:animal, false) or subtypeOf($test:type)]", + paramDefs, + namespacePrefixResolver, + false); + assertEquals(1, answer.size()); + assertFalse("Incorrect result: search root node pulled back", answer.contains(n1Ref)); + assertTrue("Incorrect result: incorrect node retrieved", answer.contains(n3Ref)); + } + + public void testJCRRoot() throws Exception + { + + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + NodeServiceXPath xpath; + List list; + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + // create the document navigator + DocumentNavigator documentNavigator = new DocumentNavigator( + dictionaryService, + nodeService, + searcher, + namespacePrefixResolver, + false, true); + + xpath = new NodeServiceXPath("/jcr:root", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root/*", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(2, list.size()); + + xpath = new NodeServiceXPath("/*/*", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(2, list.size()); + } + + public void testBooleanFunctions() throws Exception + { + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + NodeServiceXPath xpath; + List list; + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + // create the document navigator + DocumentNavigator documentNavigator = new DocumentNavigator( + dictionaryService, + nodeService, + searcher, + namespacePrefixResolver, + false, true); + + xpath = new NodeServiceXPath("/jcr:root[true()]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root[false()]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root[not(true())]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root[not(false())]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + } + + public void testMutiValueProperties() throws Exception + { + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + NodeServiceXPath xpath; + List list; + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + // create the document navigator + DocumentNavigator documentNavigator = new DocumentNavigator( + dictionaryService, + nodeService, + searcher, + namespacePrefixResolver, + false, true); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp = 'first']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp = 'second']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp = 'third']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp != 'third']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp < 'e']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp > 'e']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp < 'first']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp <= 'first']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp > 'third']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvp >= 'third']", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvi < 1]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvi <= 1]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvi > 3]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(0, list.size()); + + xpath = new NodeServiceXPath("/jcr:root//*[@test:mvi >= 3]", documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + } + + public void testElementNodeTest() throws Exception + { + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + NodeServiceXPath xpath; + List list; + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + // create the document navigator + DocumentNavigator documentNavigator = new DocumentNavigator( + dictionaryService, + nodeService, + searcher, + namespacePrefixResolver, + false, true); + + xpath = new NodeServiceXPath("//element(*, *)".replaceAll("element\\(\\s*(\\*|\\$?\\w*:\\w*)\\s*,\\s*(\\*|\\$?\\w*:\\w*)\\s*\\)", "$1[subtypeOf(\"$2\")]"), documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(12, list.size()); + + xpath = new NodeServiceXPath("//element(jcr:root, *)".replaceAll("element\\(\\s*(\\*|\\$?\\w*:\\w*)\\s*,\\s*(\\*|\\$?\\w*:\\w*)\\s*\\)", "$1[subtypeOf(\"$2\")]"), documentNavigator, null); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + QueryParameterDefImpl paramDef; + + paramDef = new QueryParameterDefImpl( + QName.createQName("test:type", namespacePrefixResolver), + dictionaryService.getDataType(DataTypeDefinition.QNAME), + true, + BaseNodeServiceTest.TYPE_QNAME_TEST_CONTENT.toPrefixString(namespacePrefixResolver)); + xpath = new NodeServiceXPath("//element(*, test:content)".replaceAll("element\\(\\s*(\\*|\\$?\\w*:\\w*)\\s*,\\s*(\\*|\\$?\\w*:\\w*)\\s*\\)", "$1[subtypeOf(\"$2\")]"), documentNavigator, new QueryParameterDefinition[]{paramDef}); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(2, list.size()); + + paramDef = new QueryParameterDefImpl( + QName.createQName("test:type", namespacePrefixResolver), + dictionaryService.getDataType(DataTypeDefinition.QNAME), + true, + BaseNodeServiceTest.TYPE_QNAME_TEST_CONTENT.toPrefixString(namespacePrefixResolver)); + xpath = new NodeServiceXPath("//element(test:n6_p_n8, test:content)".replaceAll("element\\(\\s*(\\*|\\$?\\w*:\\w*)\\s*,\\s*(\\*|\\$?\\w*:\\w*)\\s*\\)", "$1[subtypeOf(\"$2\")]"), documentNavigator, new QueryParameterDefinition[]{paramDef}); + list = xpath.selectNodes(new ChildAssociationRef(null, null, null, rootNodeRef)); + assertEquals(1, list.size()); + + } + + public void testJCRLike() throws Exception + { + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + // create the document navigator +// DocumentNavigator documentNavigator = new DocumentNavigator( +// dictionaryService, +// nodeService, +// searcher, +// namespacePrefixResolver, +// false, true); + + + List answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:like(@test:animal, 'm__k%')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + } + + public void testJCRScore() throws Exception + { + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + // create the document navigator +// DocumentNavigator documentNavigator = new DocumentNavigator( +// dictionaryService, +// nodeService, +// searcher, +// namespacePrefixResolver, +// false, true); + + List answer; + + answer = searcher.selectNodes( + rootNodeRef, + "//.", + null, + namespacePrefixResolver, false); + assertEquals(9, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//.[jcr:score() = 1.0]", + null, + namespacePrefixResolver, false); + assertEquals(9, answer.size()); + } + + public void testJCRContains() throws Exception + { + BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef); + // commit the node graph + txn.commit(); + + txn = transactionService.getUserTransaction(); + txn.begin(); + + + DynamicNamespacePrefixResolver namespacePrefixResolver = new DynamicNamespacePrefixResolver(null); + namespacePrefixResolver.registerNamespace("jcr", "http://www.jcp.org/jcr/1.0"); + namespacePrefixResolver.registerNamespace(BaseNodeServiceTest.TEST_PREFIX, BaseNodeServiceTest.NAMESPACE); + // create the document navigator +// DocumentNavigator documentNavigator = new DocumentNavigator( +// dictionaryService, +// nodeService, +// searcher, +// namespacePrefixResolver, +// false, true); + + List answer; + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text1, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + + + + + + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text3, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text3, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text3, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text3, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text3, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text2, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text12, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text13, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text23, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(0, answer.size()); + + + + + + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(@test:text123, 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'bun')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'bun cake')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'bun biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + answer = searcher.selectNodes( + rootNodeRef, + "//*[jcr:contains(., 'bun cake biscuit')]", + null, + namespacePrefixResolver, false); + assertEquals(1, answer.size()); + + } +} diff --git a/source/java/org/alfresco/repo/search/SearcherException.java b/source/java/org/alfresco/repo/search/SearcherException.java new file mode 100644 index 0000000000..9d7d09e86d --- /dev/null +++ b/source/java/org/alfresco/repo/search/SearcherException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search; + +/** + * Searcher related exceptions + * + * @author andyh + * + */ +public class SearcherException extends RuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = 3905522713513899318L; + + public SearcherException() + { + super(); + } + + public SearcherException(String message) + { + super(message); + } + + public SearcherException(String message, Throwable cause) + { + super(message, cause); + } + + public SearcherException(Throwable cause) + { + super(cause); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/JCR170Searcher.java b/source/java/org/alfresco/repo/search/impl/JCR170Searcher.java new file mode 100644 index 0000000000..ec14226d42 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/JCR170Searcher.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl; + +import org.alfresco.repo.search.AbstractSearcherComponent; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.QueryParameter; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.namespace.QName; + +/** + * Simple searcher against another store using the JSR 170 API. + *

    + * This class is not fully implemented and hence still abstract. + */ +public abstract class JCR170Searcher extends AbstractSearcherComponent +{ + public ResultSet query(StoreRef store, String language, String query, Path[] queryOptions, + QueryParameter[] queryParameters) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public ResultSet query(StoreRef store, String language, String query, Path[] attributePaths, QueryParameterDefinition[] queryParameterDefinitions) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public ResultSet query(StoreRef store, QName queryId, QueryParameter[] queryParameters) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public ResultSet query(SearchParameters searchParameters) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/NoActionIndexer.java b/source/java/org/alfresco/repo/search/impl/NoActionIndexer.java new file mode 100644 index 0000000000..629a2e9ef3 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/NoActionIndexer.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl; + +import org.alfresco.repo.search.Indexer; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * A no action indexer - the indexing is done automatically along with + * persistence + * + * TODO: Rename to Adaptor? + * + * @author andyh + * + */ +public class NoActionIndexer implements Indexer +{ + + public void createNode(ChildAssociationRef relationshipRef) + { + return; + } + + public void updateNode(NodeRef nodeRef) + { + return; + } + + public void deleteNode(ChildAssociationRef relationshipRef) + { + return; + } + + public void createChildRelationship(ChildAssociationRef relationshipRef) + { + return; + } + + public void updateChildRelationship(ChildAssociationRef relationshipBeforeRef, ChildAssociationRef relationshipAfterRef) + { + return; + } + + public void deleteChildRelationship(ChildAssociationRef relationshipRef) + { + return; + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/NodeSearcher.java b/source/java/org/alfresco/repo/search/impl/NodeSearcher.java new file mode 100644 index 0000000000..6497b0f10a --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/NodeSearcher.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; + +import org.alfresco.repo.search.DocumentNavigator; +import org.alfresco.repo.search.NodeServiceXPath; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.XPathException; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.jaxen.JaxenException; + +/** + * Helper class that walks a node hierarchy. + *

    + * Some searcher methods on + * {@link org.alfresco.service.cmr.search.SearchService} can use this directly + * as its only dependencies are + * {@link org.alfresco.service.cmr.repository.NodeService}, + * {@link org.alfresco.service.cmr.dictionary.DictionaryService} and a + * {@link org.alfresco.service.cmr.search.SearchService} + * + * @author Derek Hulley + */ +public class NodeSearcher +{ + private NodeService nodeService; + + private DictionaryService dictionaryService; + + private SearchService searchService; + + public NodeSearcher(NodeService nodeService, DictionaryService dictionaryService, SearchService searchService) + { + this.nodeService = nodeService; + this.dictionaryService = dictionaryService; + this.searchService = searchService; + } + + /** + * @see NodeServiceXPath + */ + public synchronized List selectNodes(NodeRef contextNodeRef, String xpathIn, + QueryParameterDefinition[] paramDefs, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks, String language) + { + try + { + String xpath = xpathIn; + boolean useJCRXPath = language.equalsIgnoreCase(SearchService.LANGUAGE_JCR_XPATH); + + List order = null; + + // replace element + if (useJCRXPath) + { + order = new ArrayList(); + // We do not allow variable substitution with this pattern + xpath = xpath.replaceAll("element\\(\\s*(\\*|\\w*:\\w*)\\s*,\\s*(\\*|\\w*:\\w*)\\s*\\)", + "$1[subtypeOf(\"$2\")]"); + String split[] = xpath.split("order\\s*by\\s*", 2); + xpath = split[0]; + + if (split.length > 1 && split[1].length() > 0) + { + String clauses[] = split[1].split("\\s,\\s"); + + for (String clause : clauses) + { + if (clause.startsWith("@")) + { + String attribute = clause.replaceFirst("@(\\p{Alpha}[\\w:]*)(?:\\s+(.*))?", "$1"); + String sort = clause.replaceFirst("@(\\p{Alpha}[\\w:]*)(?:\\s+(.*))?", "$2"); + + if (sort.length() == 0) + { + sort = "ascending"; + } + + QName attributeQName = QName.createQName(attribute, namespacePrefixResolver); + order.add(new AttributeOrder(attributeQName, sort.equalsIgnoreCase("ascending"))); + } + else if (clause.startsWith("jcr:score")) + { + // ignore jcr:score ordering + } + else + { + throw new IllegalArgumentException("Malformed order by expression " + split[1]); + } + } + + } + + } + + DocumentNavigator documentNavigator = new DocumentNavigator(dictionaryService, nodeService, searchService, + namespacePrefixResolver, followAllParentLinks, useJCRXPath); + NodeServiceXPath nsXPath = new NodeServiceXPath(xpath, documentNavigator, paramDefs); + for (String prefix : namespacePrefixResolver.getPrefixes()) + { + nsXPath.addNamespace(prefix, namespacePrefixResolver.getNamespaceURI(prefix)); + } + List list = nsXPath.selectNodes(nodeService.getPrimaryParent(contextNodeRef)); + HashSet unique = new HashSet(list.size()); + for (Object o : list) + { + if (o instanceof ChildAssociationRef) + { + unique.add(((ChildAssociationRef) o).getChildRef()); + } + else if (o instanceof DocumentNavigator.Property) + { + unique.add(((DocumentNavigator.Property) o).parent); + } + else + { + throw new XPathException("Xpath expression must only select nodes"); + } + } + + List answer = new ArrayList(unique.size()); + answer.addAll(unique); + if (order != null) + { + orderNodes(answer, order); + for(NodeRef node : answer) + { + StringBuffer buffer = new StringBuffer(); + for (AttributeOrder attOrd : order) + { + buffer.append(" ").append(nodeService.getProperty(node, attOrd.attribute)); + } + System.out.println(buffer.toString()); + } + System.out.println(); + } + return answer; + } + catch (JaxenException e) + { + throw new XPathException("Error executing xpath: \n" + " xpath: " + xpathIn, e); + } + } + + private void orderNodes(List answer, List order) + { + Collections.sort(answer, new NodeRefComparator(nodeService, order)); + } + + static class NodeRefComparator implements Comparator + { + List order; + NodeService nodeService; + + NodeRefComparator(NodeService nodeService, List order) + { + this.nodeService = nodeService; + this.order = order; + } + + @SuppressWarnings("unchecked") + public int compare(NodeRef n1, NodeRef n2) + { + for (AttributeOrder attributeOrder : order) + { + Serializable o1 = nodeService.getProperty(n1, attributeOrder.attribute); + Serializable o2 = nodeService.getProperty(n2, attributeOrder.attribute); + + if (o1 == null) + { + if (o2 == null) + { + continue; + } + else + { + return attributeOrder.ascending ? -1 : 1; + } + } + else + { + if (o2 == null) + { + return attributeOrder.ascending ? 1 : -1; + } + else + { + if ((o1 instanceof Comparable) && (o2 instanceof Comparable)) + { + return (attributeOrder.ascending ? 1 : -1) * ((Comparable)o1).compareTo((Comparable) o2); + } + else + { + continue; + } + } + } + + } + return 0; + } + } + + /** + * @see NodeServiceXPath + */ + public List selectProperties(NodeRef contextNodeRef, String xpath, + QueryParameterDefinition[] paramDefs, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks, String language) + { + try + { + boolean useJCRXPath = language.equalsIgnoreCase(SearchService.LANGUAGE_JCR_XPATH); + + DocumentNavigator documentNavigator = new DocumentNavigator(dictionaryService, nodeService, searchService, + namespacePrefixResolver, followAllParentLinks, useJCRXPath); + NodeServiceXPath nsXPath = new NodeServiceXPath(xpath, documentNavigator, paramDefs); + for (String prefix : namespacePrefixResolver.getPrefixes()) + { + nsXPath.addNamespace(prefix, namespacePrefixResolver.getNamespaceURI(prefix)); + } + List list = nsXPath.selectNodes(nodeService.getPrimaryParent(contextNodeRef)); + List answer = new ArrayList(list.size()); + for (Object o : list) + { + if (!(o instanceof DocumentNavigator.Property)) + { + throw new XPathException("Xpath expression must only select nodes"); + } + answer.add(((DocumentNavigator.Property) o).value); + } + return answer; + } + catch (JaxenException e) + { + throw new XPathException("Error executing xpath", e); + } + } + + private static class AttributeOrder + { + QName attribute; + + boolean ascending; + + AttributeOrder(QName attribute, boolean ascending) + { + this.attribute = attribute; + this.ascending = ascending; + } + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/CharStream.java b/source/java/org/alfresco/repo/search/impl/lucene/CharStream.java new file mode 100644 index 0000000000..0e11c043db --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/CharStream.java @@ -0,0 +1,110 @@ +/* Generated By:JavaCC: Do not edit this line. CharStream.java Version 3.0 */ +package org.alfresco.repo.search.impl.lucene; + +/** + * This interface describes a character stream that maintains line and + * column number positions of the characters. It also has the capability + * to backup the stream to some extent. An implementation of this + * interface is used in the TokenManager implementation generated by + * JavaCCParser. + * + * All the methods except backup can be implemented in any fashion. backup + * needs to be implemented correctly for the correct operation of the lexer. + * Rest of the methods are all used to get information like line number, + * column number and the String that constitutes a token and are not used + * by the lexer. Hence their implementation won't affect the generated lexer's + * operation. + */ + +public interface CharStream { + + /** + * Returns the next character from the selected input. The method + * of selecting the input is the responsibility of the class + * implementing this interface. Can throw any java.io.IOException. + */ + char readChar() throws java.io.IOException; + + /** + * Returns the column position of the character last read. + * @deprecated + * @see #getEndColumn + */ + int getColumn(); + + /** + * Returns the line number of the character last read. + * @deprecated + * @see #getEndLine + */ + int getLine(); + + /** + * Returns the column number of the last character for current token (being + * matched after the last call to BeginTOken). + */ + int getEndColumn(); + + /** + * Returns the line number of the last character for current token (being + * matched after the last call to BeginTOken). + */ + int getEndLine(); + + /** + * Returns the column number of the first character for current token (being + * matched after the last call to BeginTOken). + */ + int getBeginColumn(); + + /** + * Returns the line number of the first character for current token (being + * matched after the last call to BeginTOken). + */ + int getBeginLine(); + + /** + * Backs up the input stream by amount steps. Lexer calls this method if it + * had already read some characters, but could not use them to match a + * (longer) token. So, they will be used again as the prefix of the next + * token and it is the implemetation's responsibility to do this right. + */ + void backup(int amount); + + /** + * Returns the next character that marks the beginning of the next token. + * All characters must remain in the buffer between two successive calls + * to this method to implement backup correctly. + */ + char BeginToken() throws java.io.IOException; + + /** + * Returns a string made up of characters from the marked token beginning + * to the current buffer position. Implementations have the choice of returning + * anything that they want to. For example, for efficiency, one might decide + * to just return null, which is a valid implementation. + */ + String GetImage(); + + /** + * Returns an array of characters that make up the suffix of length 'len' for + * the currently matched token. This is used to build up the matched string + * for use in actions in the case of MORE. A simple and inefficient + * implementation of this is as follows : + * + * { + * String t = GetImage(); + * return t.substring(t.length() - len, t.length()).toCharArray(); + * } + */ + char[] GetSuffix(int len); + + /** + * The lexer calls this function to indicate that it is done with the stream + * and hence implementations can free any resources held by this class. + * Again, the body of this function can be just empty and it will not + * affect the lexer's operation. + */ + void Done(); + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/ClosingIndexSearcher.java b/source/java/org/alfresco/repo/search/impl/lucene/ClosingIndexSearcher.java new file mode 100644 index 0000000000..60e6753f28 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/ClosingIndexSearcher.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.Directory; + +public class ClosingIndexSearcher extends IndexSearcher +{ + IndexReader reader; + + public ClosingIndexSearcher(String path) throws IOException + { + super(path); + } + + public ClosingIndexSearcher(Directory directory) throws IOException + { + super(directory); + } + + public ClosingIndexSearcher(IndexReader r) + { + super(r); + this.reader = r; + } + + @Override + public void close() throws IOException + { + super.close(); + if(reader != null) + { + reader.close(); + } + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/DebugXPathHandler.java b/source/java/org/alfresco/repo/search/impl/lucene/DebugXPathHandler.java new file mode 100644 index 0000000000..35453845de --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/DebugXPathHandler.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.saxpath.Axis; +import org.saxpath.SAXPathException; +import org.saxpath.XPathHandler; + +import com.werken.saxpath.XPathReader; + +public class DebugXPathHandler implements XPathHandler +{ + + public DebugXPathHandler() + { + super(); + // TODO Auto-generated constructor stub + } + + public void endAbsoluteLocationPath() throws SAXPathException + { + System.out.println("End Absolute Location Path"); + } + + public void endAdditiveExpr(int arg0) throws SAXPathException + { + System.out.println("End Additive Expr: value = " + arg0); + } + + public void endAllNodeStep() throws SAXPathException + { + System.out.println("End All Node Step"); + } + + public void endAndExpr(boolean arg0) throws SAXPathException + { + System.out.println("End And Expr: value = " + arg0); + } + + public void endCommentNodeStep() throws SAXPathException + { + System.out.println("End Comment Node Step"); + } + + public void endEqualityExpr(int arg0) throws SAXPathException + { + System.out.println("End Equality Expr: value = " + arg0); + } + + public void endFilterExpr() throws SAXPathException + { + System.out.println("End Filter Expr"); + } + + public void endFunction() throws SAXPathException + { + System.out.println("End Function"); + } + + public void endMultiplicativeExpr(int arg0) throws SAXPathException + { + System.out.println("End Multiplicative Expr: value = " + arg0); + } + + public void endNameStep() throws SAXPathException + { + System.out.println("End Name Step"); + } + + public void endOrExpr(boolean arg0) throws SAXPathException + { + System.out.println("End Or Expr: value = " + arg0); + } + + public void endPathExpr() throws SAXPathException + { + System.out.println("End Path Expression"); + } + + public void endPredicate() throws SAXPathException + { + System.out.println("End Predicate"); + } + + public void endProcessingInstructionNodeStep() throws SAXPathException + { + System.out.println("End Processing Instruction Node Step"); + } + + public void endRelationalExpr(int arg0) throws SAXPathException + { + System.out.println("End Relational Expr: value = " + arg0); + } + + public void endRelativeLocationPath() throws SAXPathException + { + System.out.println("End Relative Location Path"); + } + + public void endTextNodeStep() throws SAXPathException + { + System.out.println("End Text Node Step"); + } + + public void endUnaryExpr(int arg0) throws SAXPathException + { + System.out.println("End Unary Expr: value = " + arg0); + } + + public void endUnionExpr(boolean arg0) throws SAXPathException + { + System.out.println("End Union Expr: value = " + arg0); + } + + public void endXPath() throws SAXPathException + { + System.out.println("End XPath"); + } + + public void literal(String arg0) throws SAXPathException + { + System.out.println("Literal = " + arg0); + } + + public void number(double arg0) throws SAXPathException + { + System.out.println("Double = " + arg0); + } + + public void number(int arg0) throws SAXPathException + { + System.out.println("Integer = " + arg0); + } + + public void startAbsoluteLocationPath() throws SAXPathException + { + System.out.println("Start Absolute Location Path"); + } + + public void startAdditiveExpr() throws SAXPathException + { + System.out.println("Start Additive Expression"); + } + + public void startAllNodeStep(int arg0) throws SAXPathException + { + System.out.println("Start All Node Exp: Axis = " + Axis.lookup(arg0)); + } + + public void startAndExpr() throws SAXPathException + { + System.out.println("Start AndExpression"); + } + + public void startCommentNodeStep(int arg0) throws SAXPathException + { + System.out.println("Start Comment Node Step"); + } + + public void startEqualityExpr() throws SAXPathException + { + System.out.println("Start Equality Expression"); + } + + public void startFilterExpr() throws SAXPathException + { + System.out.println("Start Filter Expression"); + } + + public void startFunction(String arg0, String arg1) throws SAXPathException + { + System.out.println("Start Function arg0 = < " + arg0 + " > arg1 = < " + arg1 + " >"); + } + + public void startMultiplicativeExpr() throws SAXPathException + { + System.out.println("Start Multiplicative Expression"); + } + + public void startNameStep(int arg0, String arg1, String arg2) throws SAXPathException + { + System.out.println("Start Name Step Axis = <" + Axis.lookup(arg0) + " > arg1 = < " + arg1 + " > arg 2 <" + arg2 + + " >"); + } + + public void startOrExpr() throws SAXPathException + { + System.out.println("Start Or Expression"); + } + + public void startPathExpr() throws SAXPathException + { + System.out.println("Start Path Expression"); + } + + public void startPredicate() throws SAXPathException + { + System.out.println("Start Predicate"); + } + + public void startProcessingInstructionNodeStep(int arg0, String arg1) throws SAXPathException + { + System.out.println("Start Processing INstruction Node Step = < " + arg0 + " > arg1 = < " + arg1 + " >"); + } + + public void startRelationalExpr() throws SAXPathException + { + System.out.println("Start Relationship Expression"); + } + + public void startRelativeLocationPath() throws SAXPathException + { + System.out.println("Start Relative Location Path"); + } + + public void startTextNodeStep(int arg0) throws SAXPathException + { + System.out.println("Start Text Node Step: value = " + arg0); + } + + public void startUnaryExpr() throws SAXPathException + { + System.out.println("Start Unary Expression"); + } + + public void startUnionExpr() throws SAXPathException + { + System.out.println("Start Union Expression"); + } + + public void startXPath() throws SAXPathException + { + System.out.println("Start XPath"); + } + + public void variableReference(String arg0, String arg1) throws SAXPathException + { + System.out.println("Variable Reference arg0 = < " + arg0 + " > arg1 = < " + arg1); + } + + /** + * @param args + * @throws SAXPathException + */ + public static void main(String[] args) throws SAXPathException + { + XPathReader reader = new XPathReader(); + reader.setXPathHandler(new DebugXPathHandler()); + reader + .parse("/ns:one[@woof='dog']/two/./../two[functionTest(@a, @b, $woof:woof)]/three/*/four//*/five/six[@exists1 and @exists2]"); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/FastCharStream.java b/source/java/org/alfresco/repo/search/impl/lucene/FastCharStream.java new file mode 100644 index 0000000000..04d659a096 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/FastCharStream.java @@ -0,0 +1,122 @@ +// FastCharStream.java +package org.alfresco.repo.search.impl.lucene; + +/** + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.IOException; +import java.io.Reader; + +/** An efficient implementation of JavaCC's CharStream interface.

    Note that + * this does not do line-number counting, but instead keeps track of the + * character position of the token in the input, as required by Lucene's {@link + * org.apache.lucene.analysis.Token} API. */ +public final class FastCharStream implements CharStream { + char[] buffer = null; + + int bufferLength = 0; // end of valid chars + int bufferPosition = 0; // next char to read + + int tokenStart = 0; // offset in buffer + int bufferStart = 0; // position in file of buffer + + Reader input; // source of chars + + /** Constructs from a Reader. */ + public FastCharStream(Reader r) { + input = r; + } + + public final char readChar() throws IOException { + if (bufferPosition >= bufferLength) + refill(); + return buffer[bufferPosition++]; + } + + private final void refill() throws IOException { + int newPosition = bufferLength - tokenStart; + + if (tokenStart == 0) { // token won't fit in buffer + if (buffer == null) { // first time: alloc buffer + buffer = new char[2048]; + } else if (bufferLength == buffer.length) { // grow buffer + char[] newBuffer = new char[buffer.length*2]; + System.arraycopy(buffer, 0, newBuffer, 0, bufferLength); + buffer = newBuffer; + } + } else { // shift token to front + System.arraycopy(buffer, tokenStart, buffer, 0, newPosition); + } + + bufferLength = newPosition; // update state + bufferPosition = newPosition; + bufferStart += tokenStart; + tokenStart = 0; + + int charsRead = // fill space in buffer + input.read(buffer, newPosition, buffer.length-newPosition); + if (charsRead == -1) + throw new IOException("read past eof"); + else + bufferLength += charsRead; + } + + public final char BeginToken() throws IOException { + tokenStart = bufferPosition; + return readChar(); + } + + public final void backup(int amount) { + bufferPosition -= amount; + } + + public final String GetImage() { + return new String(buffer, tokenStart, bufferPosition - tokenStart); + } + + public final char[] GetSuffix(int len) { + char[] value = new char[len]; + System.arraycopy(buffer, bufferPosition - len, value, 0, len); + return value; + } + + public final void Done() { + try { + input.close(); + } catch (IOException e) { + System.err.println("Caught: " + e + "; ignoring."); + } + } + + public final int getColumn() { + return bufferStart + bufferPosition; + } + public final int getLine() { + return 1; + } + public final int getEndColumn() { + return bufferStart + bufferPosition; + } + public final int getEndLine() { + return 1; + } + public final int getBeginColumn() { + return bufferStart + tokenStart; + } + public final int getBeginLine() { + return 1; + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/FilterIndexReaderByNodeRefs.java b/source/java/org/alfresco/repo/search/impl/lucene/FilterIndexReaderByNodeRefs.java new file mode 100644 index 0000000000..4120a69d22 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/FilterIndexReaderByNodeRefs.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; +import java.util.BitSet; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.lucene.index.FilterIndexReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.index.TermPositions; + +public class FilterIndexReaderByNodeRefs extends FilterIndexReader +{ + BitSet deletedDocuments; + + public FilterIndexReaderByNodeRefs(IndexReader reader, Set deletions) + { + super(reader); + deletedDocuments = new BitSet(reader.maxDoc()); + + try + { + for (NodeRef nodeRef : deletions) + { + TermDocs td = reader.termDocs(new Term("ID", nodeRef.toString())); + while (td.next()) + { + deletedDocuments.set(td.doc(), true); + } + } + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to construct filtering index reader", e); + } + } + + public static class FilterTermDocs implements TermDocs + { + BitSet deletedDocuments; + + protected TermDocs in; + + public FilterTermDocs(TermDocs in, BitSet deletedDocuments) + { + this.in = in; + this.deletedDocuments = deletedDocuments; + } + + public void seek(Term term) throws IOException + { + // Seek is left to the base implementation + in.seek(term); + } + + public void seek(TermEnum termEnum) throws IOException + { + // Seek is left to the base implementation + in.seek(termEnum); + } + + public int doc() + { + // The current document info is valid in the base implementation + return in.doc(); + } + + public int freq() + { + // The frequency is valid in the base implementation + return in.freq(); + } + + public boolean next() throws IOException + { + while(in.next()) + { + if(!deletedDocuments.get(in.doc())) + { + // Not masked + return true; + } + } + return false; + } + + public int read(int[] docs, int[] freqs) throws IOException + { + int[] innerDocs = new int[docs.length]; + int[] innerFreq = new int[docs.length]; + int count = in.read(innerDocs, innerFreq); + + // Is the stream exhausted + if (count == 0) + { + return 0; + } + + if(allDeleted(innerDocs, count)) + { + // Did not find anything - try again + return read(docs, freqs); + } + + // Add non deleted + + int insertPosition = 0; + for(int i = 0; i < count; i++) + { + if(!deletedDocuments.get(innerDocs[i])) + { + docs[insertPosition] = innerDocs[i]; + freqs[insertPosition] = innerFreq[i]; + insertPosition++; + } + } + + return insertPosition; + } + + private boolean allDeleted(int[] docs, int fillSize) + { + for(int i = 0; i < fillSize; i++) + { + if(!deletedDocuments.get(docs[i])) + { + return false; + } + } + return true; + } + + public boolean skipTo(int i) throws IOException + { + boolean result = in.skipTo(i); + if(result == false) + { + return false; + } + + if(deletedDocuments.get(in.doc())) + { + return skipTo(i); + } + else + { + return true; + } + } + + public void close() throws IOException + { + // Leave to internal implementation + in.close(); + } + } + + /** Base class for filtering {@link TermPositions} implementations. */ + public static class FilterTermPositions extends FilterTermDocs implements TermPositions + { + + public FilterTermPositions(TermPositions in, BitSet deletedDocuements) + { + super(in, deletedDocuements); + } + + public int nextPosition() throws IOException + { + return ((TermPositions) this.in).nextPosition(); + } + } + + @Override + public int numDocs() + { + return super.numDocs() - deletedDocuments.cardinality(); + } + + @Override + public TermDocs termDocs() throws IOException + { + return new FilterTermDocs(super.termDocs(), deletedDocuments); + } + + @Override + public TermPositions termPositions() throws IOException + { + return new FilterTermPositions(super.termPositions(), deletedDocuments); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/Lockable.java b/source/java/org/alfresco/repo/search/impl/lucene/Lockable.java new file mode 100644 index 0000000000..bd26a963c5 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/Lockable.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.alfresco.repo.search.transaction.LuceneIndexLock; + +public interface Lockable +{ + public void setLuceneIndexLock(LuceneIndexLock luceneIndexLock); + + public LuceneIndexLock getLuceneIndexLock(); + + public void getReadLock(); + + public void releaseReadLock(); + + public void getWriteLock(); + + public void releaseWriteLock(); +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneAnalyser.java new file mode 100644 index 0000000000..732010a3d8 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneAnalyser.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.search.impl.lucene.analysis.PathAnalyser; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.namespace.QName; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.WhitespaceAnalyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; + +/** + * Analyse properties according to the property definition. + * + * The default is to use the standard tokeniser. The tokeniser should not have + * been called when indexeing properties that require no tokenisation. (tokenise + * should be set to false when adding the field to the document) + * + * @author andyh + * + */ + +public class LuceneAnalyser extends Analyzer +{ + + private DictionaryService dictionaryService; + + private Analyzer defaultAnalyser; + + private Map analysers = new HashMap(); + + /** + * Constructs with a default standard analyser + * + * @param defaultAnalyzer + * Any fields not specifically defined to use a different + * analyzer will use the one provided here. + */ + public LuceneAnalyser(DictionaryService dictionaryService) + { + this(new StandardAnalyzer()); + this.dictionaryService = dictionaryService; + } + + /** + * Constructs with default analyzer. + * + * @param defaultAnalyzer + * Any fields not specifically defined to use a different + * analyzer will use the one provided here. + */ + public LuceneAnalyser(Analyzer defaultAnalyser) + { + this.defaultAnalyser = defaultAnalyser; + } + + public TokenStream tokenStream(String fieldName, Reader reader) + { + Analyzer analyser = (Analyzer) analysers.get(fieldName); + if (analyser == null) + { + analyser = findAnalyser(fieldName); + } + return analyser.tokenStream(fieldName, reader); + } + + private Analyzer findAnalyser(String fieldName) + { + Analyzer analyser; + if (fieldName.equals("PATH")) + { + analyser = new PathAnalyser(); + } + else if (fieldName.equals("QNAME")) + { + analyser = new PathAnalyser(); + } + else if (fieldName.equals("TYPE")) + { + throw new UnsupportedOperationException("TYPE must not be tokenised"); + } + else if (fieldName.equals("ASPECT")) + { + throw new UnsupportedOperationException("ASPECT must not be tokenised"); + } + else if (fieldName.equals("ANCESTOR")) + { + analyser = new WhitespaceAnalyzer(); + } + else if (fieldName.startsWith("@")) + { + QName propertyQName = QName.createQName(fieldName.substring(1)); + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + DataTypeDefinition dataType = (propertyDef == null) ? dictionaryService.getDataType(DataTypeDefinition.TEXT) : propertyDef.getDataType(); + String analyserClassName = dataType.getAnalyserClassName(); + try + { + Class clazz = Class.forName(analyserClassName); + analyser = (Analyzer)clazz.newInstance(); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Unable to load analyser for property " + fieldName.substring(1) + " of type " + dataType.getName() + " using " + analyserClassName); + } + catch (InstantiationException e) + { + throw new RuntimeException("Unable to load analyser for property " + fieldName.substring(1) + " of type " + dataType.getName() + " using " + analyserClassName); + } + catch (IllegalAccessException e) + { + throw new RuntimeException("Unable to load analyser for property " + fieldName.substring(1) + " of type " + dataType.getName() + " using " + analyserClassName); + } + } + else + { + analyser = defaultAnalyser; + } + analysers.put(fieldName, analyser); + return analyser; + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneBase.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneBase.java new file mode 100644 index 0000000000..bd85d76239 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneBase.java @@ -0,0 +1,1019 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import org.alfresco.repo.search.IndexerException; +import org.alfresco.repo.search.transaction.LuceneIndexLock; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.apache.log4j.Logger; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.MultiReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; + +/** + * Common support for abstracting the lucene indexer from its configuration and + * management requirements. + * + *

    + * This class defines where the indexes are stored. This should be via a + * configurable Bean property in Spring. + * + *

    + * The default file structure is + *

      + *
    1. "base"/"protocol"/"name"/ for the main index + *
    2. "base"/"protocol"/"name"/deltas/"id" for transactional updates + *
    3. "base"/"protocol"/"name"/undo/"id" undo information + *
    + * + *

    + * The IndexWriter and IndexReader for a given index are toggled (one should be + * used for delete and the other for write). These are reused/closed/initialised + * as required. + * + *

    + * The index deltas are buffered to memory and persisted in the file system as + * required. + * + * @author Andy Hind + * + */ + +public abstract class LuceneBase implements Lockable +{ + private static Logger s_logger = Logger.getLogger(LuceneBase.class); + + /** + * The base directory for the index (on file) + */ + + private File baseDir; + + /** + * The directory for deltas (on file) + */ + + private File deltaDir; + + /** + * The directory for undo information (on file) + */ + + private File undoDir; + + /** + * The index reader for the on file delta. (This should no coexist with the + * writer) + */ + + private IndexReader deltaReader; + + /** + * The writer for the delta to file. (This should no coexist with the + * reader) + */ + + private IndexWriter deltaWriter; + + /** + * The writer for the main index. (This should no coexist with the reader) + */ + + private IndexWriter mainWriter; + + /* + * TODO: The main indexer operations need to be serialised to the main index + */ + + /** + * The reader for the main index. (This should no coexist with the writer) + */ + + private IndexReader mainReader; + + /** + * The identifier for the store + */ + + protected StoreRef store; + + /** + * The identifier for the delta + */ + + protected String deltaId; + + private LuceneIndexLock luceneIndexLock; + + private LuceneConfig config; + + // "lucene-indexes"; + + /** + * Initialise the configuration elements of the lucene store indexers and + * searchers. + * + * @param store + * @param deltaId + * @throws IOException + */ + protected void initialise(StoreRef store, String deltaId, boolean createMain, boolean createDelta) + throws LuceneIndexException + { + this.store = store; + this.deltaId = deltaId; + + String basePath = getMainPath(); + baseDir = new File(basePath); + if (createMain) + { + getWriteLock(); + } + try + { + try + { + initialiseFSDirectory(basePath, false, createMain).close(); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to close directory after initialisation " + basePath); + } + if (deltaId != null) + { + String deltaPath = getDeltaPath(); + deltaDir = new File(deltaPath); + try + { + initialiseFSDirectory(deltaPath, createDelta, createDelta).close(); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to close directory after initialisation " + deltaPath); + } + // undoDir = initialiseFSDirectory(basePath + File.separator + + // "undo" + File.separator + deltaId + File.separator, true, + // true); + } + } + finally + { + if (createMain) + { + releaseWriteLock(); + } + } + } + + /** + * Utility method to find the path to the transactional store for this index + * delta + * + * @return + */ + private String getDeltaPath() + { + String deltaPath = getBasePath() + File.separator + "delta" + File.separator + this.deltaId + File.separator; + return deltaPath; + } + + private String getMainPath() + { + String mainPath = getBasePath() + File.separator + "index" + File.separator; + return mainPath; + } + + /** + * Utility method to find the path to the base index + * + * @return + */ + private String getBasePath() + { + if (config.getIndexRootLocation() == null) + { + throw new IndexerException("No configuration for index location"); + } + String basePath = config.getIndexRootLocation() + + File.separator + store.getProtocol() + File.separator + store.getIdentifier() + File.separator; + return basePath; + } + + /** + * Utility method to initiliase a lucene FSDirectorya at a given location. + * We may try and delete the directory when the JVM exits. + * + * @param path + * @param temp + * @return + * @throws IOException + */ + private Directory initialiseFSDirectory(String path, boolean deleteOnExit, boolean overwrite) + throws LuceneIndexException + { + try + { + File file = new File(path); + if (overwrite) + { + // deleteDirectory(file); + } + if (!file.exists()) + { + file.mkdirs(); + if (deleteOnExit) + { + file.deleteOnExit(); + } + + return FSDirectory.getDirectory(file, true); + } + else + { + return FSDirectory.getDirectory(file, overwrite); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Filed to initialise lucene file directory " + path, e); + } + } + + /** + * Get a searcher for the main index TODO: Split out support for the main + * index. We really only need this if we want to search over the changing + * index before it is committed + * + * @return + * @throws IOException + */ + + protected IndexSearcher getSearcher() throws LuceneIndexException + { + try + { + return new IndexSearcher(getMainPath()); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to open IndexSarcher for " + getMainPath(), e); + } + } + + protected Searcher getSearcher(LuceneIndexer luceneIndexer) throws LuceneIndexException + { + // If we know the delta id we should do better + try + { + if (mainIndexExists()) + { + if (luceneIndexer == null) + { + return new IndexSearcher(getMainPath()); + } + else + { + // TODO: Create appropriate reader that lies about deletions + // from the first + // + luceneIndexer.flushPending(); + return new ClosingIndexSearcher(new MultiReader(new IndexReader[] { + new FilterIndexReaderByNodeRefs(IndexReader.open(getMainPath()), luceneIndexer + .getDeletions()), IndexReader.open(getDeltaPath()) })); + } + } + else + { + if (luceneIndexer == null) + { + return null; + } + else + { + luceneIndexer.flushPending(); + return new IndexSearcher(getDeltaPath()); + } + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to open IndexSarcher for " + getMainPath(), e); + } + } + + /** + * Get a reader for the on file portion of the delta + * + * @return + * @throws IOException + */ + + protected IndexReader getDeltaReader() throws LuceneIndexException + { + if (deltaReader == null) + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Trying to get index delta reader for tx " + deltaDir); + } + // Readers and writes can not exists at the same time so we swap + // between them. + closeDeltaWriter(); + + if (!indexExists(deltaDir)) + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("... index does not already exist for " + deltaDir + " creating ..."); + } + try + { + // Make sure there is something we can read + IndexWriter writer = new IndexWriter(deltaDir, new LuceneAnalyser(dictionaryService), true); + writer.setUseCompoundFile(true); + writer.close(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("... index created " + deltaDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to create empty index for delta reader: " + deltaDir, e); + } + } + + try + { + deltaReader = IndexReader.open(deltaDir); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Opened delta reader for " + deltaDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to open delta reader: " + deltaDir, e); + } + + } + return deltaReader; + } + + private boolean indexExists(File dir) + { + return IndexReader.indexExists(dir); + } + + /** + * Close the on file reader for the delta if it is open + * + * @throws IOException + */ + + protected void closeDeltaReader() throws LuceneIndexException + { + if (deltaReader != null) + { + try + { + try + { + deltaReader.close(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Closed delta read for " + deltaDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Filed to close delta reader " + deltaDir, e); + } + } + finally + { + deltaReader = null; + } + } + + } + + /** + * Get the on file writer for the delta + * + * @return + * @throws IOException + */ + protected IndexWriter getDeltaWriter() throws LuceneIndexException + { + if (deltaWriter == null) + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Trying to create delta writer " + deltaDir); + } + // Readers and writes can not exists at the same time so we swap + // between them. + closeDeltaReader(); + + try + { + boolean create = !IndexReader.indexExists(deltaDir); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Creating delta writer " + deltaDir + " " + (create ? "CREATE" : "OPEN")); + } + deltaWriter = new IndexWriter(deltaDir, new LuceneAnalyser(dictionaryService), create); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new IndexerException("Failed to get delta writer for " + deltaDir, e); + } + } + deltaWriter.setUseCompoundFile(true); + deltaWriter.minMergeDocs = config.getIndexerMinMergeDocs(); + deltaWriter.mergeFactor = config.getIndexerMergeFactor(); + deltaWriter.maxMergeDocs = config.getIndexerMaxMergeDocs(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Created delta writer " + deltaDir); + } + return deltaWriter; + } + + /** + * Close the on disk delta writer + * + * @throws IOException + */ + + protected void closeDeltaWriter() throws LuceneIndexException + { + if (deltaWriter != null) + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Trying to close delta writer... " + deltaDir); + } + try + { + // deltaWriter.optimize(); + deltaWriter.close(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Closed delta writer " + deltaDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to close delta writer " + deltaDir, e); + } + finally + { + deltaWriter = null; + } + } + + } + + /** + * Save the in memory delta to the disk, make sure there is nothing held in + * memory + * + * @throws IOException + */ + protected void saveDelta() throws LuceneIndexException + { + // Only one should exist so we do not need error trapping to execute the + // other + closeDeltaReader(); + closeDeltaWriter(); + } + + /** + * Get all the locks so we can expect a merge to succeed + * + * The delta should be thread local so we do not have to worry about + * contentention TODO: Worry about main index contentention of readers and + * writers @ + * @throws IOException + */ + protected void prepareToMergeIntoMain() throws LuceneIndexException + { + if (mainWriter != null) + { + throw new IndexerException("Can not merge as main writer is active"); + } + if (mainReader != null) + { + throw new IndexerException("Can not merge as main reader is active"); + } + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Getting write lock for " + baseDir + " for " + deltaDir); + } + getWriteLock(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Got write lock for " + baseDir + " for " + deltaDir); + } + try + { + getDeltaReader(); // Flush any deletes + closeDeltaReader(); + } + catch (LuceneIndexException e) + { + s_logger.error("Error", e); + releaseWriteLock(); + throw e; + } + + } + + /** + * Merge the delta in the main index. The delta still exists on disk. + * + * @param terms + * A list of terms that identifiy documents to be deleted from + * the main index before the delta os merged in. + * + * @throws IOException + */ + protected void mergeDeltaIntoMain(Set terms) throws LuceneIndexException + { + if (writeLockCount < 1) + { + throw new LuceneIndexException("Must hold the write lock to merge"); + } + + if (!indexExists(baseDir)) + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Creating base index " + baseDir); + } + try + { + mainWriter = new IndexWriter(baseDir, new LuceneAnalyser(dictionaryService), true); + mainWriter.setUseCompoundFile(true); + mainWriter.close(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Created base index " + baseDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to create empty base index at " + baseDir, e); + } + } + try + { + mainReader = IndexReader.open(baseDir); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Opened base index for deletes " + baseDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to create base index reader at " + baseDir, e); + } + try + { + // Do the deletions + try + { + if ((mainReader.numDocs() > 0) && (terms.size() > 0)) + { + for (Term term : terms) + { + try + { + mainReader.delete(term); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to delete term from main index at " + baseDir, e); + } + } + } + } + finally + { + try + { + try + { + mainReader.close(); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Completed index deletes on " + baseDir + " for " + deltaDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to close from main index reader at " + baseDir, e); + } + } + finally + { + mainReader = null; + } + } + + // Do the append + + try + { + mainWriter = new IndexWriter(baseDir, new LuceneAnalyser(dictionaryService), false); + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Opened index for append " + baseDir + " for " + deltaDir); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to open main index for append at " + baseDir, e); + } + mainWriter.setUseCompoundFile(true); + + mainWriter.minMergeDocs = config.getIndexerMinMergeDocs(); + mainWriter.mergeFactor = config.getIndexerMergeFactor(); + mainWriter.maxMergeDocs = config.getIndexerMaxMergeDocs(); + + try + { + IndexReader reader = getDeltaReader(); + if (reader.numDocs() > 0) + { + IndexReader[] readers = new IndexReader[] { reader }; + try + { + mainWriter.mergeIndexes(readers); + // mainWriter.addIndexes(readers); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to merge indexes into the main index for " + + baseDir + " merging in " + deltaDir, e); + } + // mainWriter.optimize(); + closeDeltaReader(); + } + else + { + closeDeltaReader(); + } + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Closed index after append " + baseDir + " for " + deltaDir); + } + } + finally + { + try + { + try + { + mainWriter.close(); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to cloase main index after append at " + baseDir, e); + } + } + finally + { + mainWriter = null; + } + } + } + finally + { + releaseWriteLock(); + } + } + + /** + * Delete the delta and make this instance unusable + * + * This tries to tidy up all it can. It is possible some stuff will remain + * if errors are throws else where + * + * TODO: Support for cleaning up transactions - need to support recovery and + * knowing of we are prepared + * + */ + protected void deleteDelta() throws LuceneIndexException + { + try + { + // Try and close everything + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Deleting delta " + deltaDir); + } + try + { + closeDeltaReader(); + } + catch (LuceneIndexException e) + { + s_logger.warn(e); + } + try + { + closeDeltaWriter(); + } + catch (LuceneIndexException e) + { + s_logger.warn(e); + } + + // try + // { + // deltaDir.close(); + // } + // catch (IOException e) + // { + // s_logger.warn("Failed to close delta dir", e); + // } + deltaDir = null; + + // Close the main stuff + if (mainReader != null) + { + try + { + mainReader.close(); + } + catch (IOException e) + { + s_logger.warn("Failed to close main reader", e); + } + } + mainReader = null; + + if (mainWriter != null) + { + try + { + mainWriter.close(); + } + catch (IOException e) + { + s_logger.warn("Failed to close main writer", e); + } + } + mainWriter = null; + // try + // { + // baseDir.close(); + // } + // catch (IOException e) + // { + // s_logger.warn("Failed to close base dir", e); + // } + + // Delete the delta directories + String deltaPath = getDeltaPath(); + File file = new File(deltaPath); + + deleteDirectory(file); + } + finally + { + releaseWriteLock(); + } + } + + /** + * Support to help deleting directories + * + * @param file + */ + private void deleteDirectory(File file) + { + File[] children = file.listFiles(); + if (children != null) + { + for (int i = 0; i < children.length; i++) + { + File child = children[i]; + if (child.isDirectory()) + { + deleteDirectory(child); + } + else + { + if (child.exists() && !child.delete() && child.exists()) + { + s_logger.warn("Failed to delete " + child); + } + } + } + } + if (file.exists() && !file.delete() && file.exists()) + { + s_logger.warn("Failed to delete " + file); + } + } + + public LuceneIndexLock getLuceneIndexLock() + { + return luceneIndexLock; + } + + public void setLuceneIndexLock(LuceneIndexLock luceneIndexLock) + { + this.luceneIndexLock = luceneIndexLock; + } + + public void getReadLock() + { + getLuceneIndexLock().getReadLock(store); + } + + private int writeLockCount = 0; + + public void getWriteLock() throws LuceneIndexException + { + getLuceneIndexLock().getWriteLock(store); + writeLockCount++; + // Check the main index is not locked and release if it is + // We must have the lock + try + { + if (((writeLockCount == 1) && IndexReader.indexExists(baseDir) && (IndexReader.isLocked(baseDir.getPath())))) + { + Directory dir = FSDirectory.getDirectory(baseDir, false); + IndexReader.unlock(dir); + dir.close(); + s_logger.warn("Releasing unexpected lucene index write lock for " + baseDir); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + for (StackTraceElement el : trace) + { + s_logger.warn(el.toString()); + } + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Write lock failed to check or clear any existing lucene locks", e); + } + } + + public void releaseReadLock() + { + getLuceneIndexLock().releaseReadLock(store); + } + + public void releaseWriteLock() + { + + if (writeLockCount > 0) + { + try + { + if (((writeLockCount == 1) && IndexReader.indexExists(baseDir) && (IndexReader.isLocked(baseDir + .getPath())))) + { + Directory dir = FSDirectory.getDirectory(baseDir, false); + IndexReader.unlock(dir); + dir.close(); + } + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Write lock failed to check or clear any existing lucene locks", e); + } + getLuceneIndexLock().releaseWriteLock(store); + writeLockCount--; + + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Released write lock " + baseDir + " for " + deltaDir); + } + } + } + + private DictionaryService dictionaryService; + + public boolean mainIndexExists() + { + return IndexReader.indexExists(baseDir); + } + + protected IndexReader getReader() throws LuceneIndexException + { + + if (!indexExists(baseDir)) + { + getWriteLock(); + try + { + if (!indexExists(baseDir)) + { + try + { + mainWriter = new IndexWriter(baseDir, new LuceneAnalyser(dictionaryService), true); + mainWriter.setUseCompoundFile(true); + mainWriter.close(); + mainWriter = null; + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to create empty main index", e); + } + } + } + finally + { + releaseWriteLock(); + } + } + + try + { + return IndexReader.open(baseDir); + } + catch (IOException e) + { + s_logger.error("Error", e); + throw new LuceneIndexException("Failed to open main index reader", e); + } + + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public DictionaryService getDictionaryService() + { + return dictionaryService; + } + + public void setLuceneConfig(LuceneConfig config) + { + this.config = config; + } + + public LuceneConfig getLuceneConfig() + { + return config; + } + + public String getDeltaId() + { + return deltaId; + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryServiceImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryServiceImpl.java new file mode 100644 index 0000000000..9a4282537d --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryServiceImpl.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.ISO9075; +import org.alfresco.repo.search.IndexerException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.bouncycastle.crypto.paddings.ISO7816d4Padding; + +public class LuceneCategoryServiceImpl implements CategoryService +{ + private NodeService nodeService; + + private NamespacePrefixResolver namespacePrefixResolver; + + private DictionaryService dictionaryService; + + private LuceneIndexerAndSearcher indexerAndSearcher; + + public LuceneCategoryServiceImpl() + { + super(); + } + + // Inversion of control support + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setIndexerAndSearcher(LuceneIndexerAndSearcher indexerAndSearcher) + { + this.indexerAndSearcher = indexerAndSearcher; + } + + public Collection getChildren(NodeRef categoryRef, Mode mode, Depth depth) + { + if (categoryRef == null) + { + return Collections. emptyList(); + } + ResultSet resultSet = null; + try + { + StringBuilder luceneQuery = new StringBuilder(64); + + if (!mode.equals(Mode.ALL)) + { + luceneQuery.append(mode.equals(Mode.SUB_CATEGORIES) ? "-" : "").append("PATH_WITH_REPEATS:\""); + luceneQuery.append(buildXPath(nodeService.getPath(categoryRef))).append("/"); + if (depth.equals(Depth.ANY)) + { + luceneQuery.append("/"); + } + luceneQuery.append("member").append("\" "); + } + + if (!mode.equals(Mode.MEMBERS)) + { + luceneQuery.append("PATH_WITH_REPEATS:\""); + luceneQuery.append(buildXPath(nodeService.getPath(categoryRef))).append("/"); + if (depth.equals(Depth.ANY)) + { + luceneQuery.append("/"); + } + luceneQuery.append("*").append("\" "); + } + + resultSet = indexerAndSearcher.getSearcher(categoryRef.getStoreRef(), false).query(categoryRef.getStoreRef(), "lucene", luceneQuery.toString(), null, null); + + return resultSetToChildAssocCollection(resultSet); + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + } + + private String buildXPath(Path path) + { + StringBuilder pathBuffer = new StringBuilder(64); + for (Iterator elit = path.iterator(); elit.hasNext(); /**/) + { + Path.Element element = elit.next(); + if (!(element instanceof Path.ChildAssocElement)) + { + throw new IndexerException("Confused path: " + path); + } + Path.ChildAssocElement cae = (Path.ChildAssocElement) element; + if (cae.getRef().getParentRef() != null) + { + pathBuffer.append("/"); + pathBuffer.append(getPrefix(cae.getRef().getQName().getNamespaceURI())); + pathBuffer.append(ISO9075.encode(cae.getRef().getQName().getLocalName())); + } + } + return pathBuffer.toString(); + } + + HashMap prefixLookup = new HashMap(); + + private String getPrefix(String uri) + { + String prefix = prefixLookup.get(uri); + if (prefix == null) + { + Collection prefixes = namespacePrefixResolver.getPrefixes(uri); + for (String first : prefixes) + { + prefix = first; + break; + } + + prefixLookup.put(uri, prefix); + } + if (prefix == null) + { + return ""; + } + else + { + return prefix + ":"; + } + + } + + private Collection resultSetToChildAssocCollection(ResultSet resultSet) + { + List collection = new ArrayList(); + if (resultSet != null) + { + for (ResultSetRow row : resultSet) + { + ChildAssociationRef car = nodeService.getPrimaryParent(row.getNodeRef()); + collection.add(car); + } + } + return collection; + // The caller closes the result set + } + + public Collection getCategories(StoreRef storeRef, QName aspectQName, Depth depth) + { + Collection assocs = new ArrayList(); + Set nodeRefs = getClassificationNodes(storeRef, aspectQName); + for (NodeRef nodeRef : nodeRefs) + { + assocs.addAll(getChildren(nodeRef, Mode.SUB_CATEGORIES, depth)); + } + return assocs; + } + + private Set getClassificationNodes(StoreRef storeRef, QName qname) + { + ResultSet resultSet = null; + try + { + resultSet = indexerAndSearcher.getSearcher(storeRef, false).query(storeRef, "lucene", "PATH_WITH_REPEATS:\"/" + getPrefix(qname.getNamespaceURI()) + ISO9075.encode(qname.getLocalName()) + "\"", + null, null); + + Set nodeRefs = new HashSet(resultSet.length()); + for (ResultSetRow row : resultSet) + { + nodeRefs.add(row.getNodeRef()); + } + + return nodeRefs; + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + } + + public Collection getClassifications(StoreRef storeRef) + { + ResultSet resultSet = null; + try + { + resultSet = indexerAndSearcher.getSearcher(storeRef, false).query(storeRef, "lucene", "PATH_WITH_REPEATS:\"//cm:categoryRoot/*\"", null, null); + return resultSetToChildAssocCollection(resultSet); + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + } + + public Collection getClassificationAspects() + { + List list = new ArrayList(); + for (QName aspect : dictionaryService.getAllAspects()) + { + if (dictionaryService.isSubClass(aspect, ContentModel.ASPECT_CLASSIFIABLE)) + { + list.add(aspect); + } + } + return list; + } + + public NodeRef createClassifiction(StoreRef storeRef, QName typeName, String attributeName) + { + throw new UnsupportedOperationException(); + } + + public Collection getRootCategories(StoreRef storeRef, QName aspectName) + { + Collection assocs = new ArrayList(); + Set nodeRefs = getClassificationNodes(storeRef, aspectName); + for (NodeRef nodeRef : nodeRefs) + { + assocs.addAll(getChildren(nodeRef, Mode.SUB_CATEGORIES, Depth.IMMEDIATE)); + } + return assocs; + } + + public NodeRef createCategory(NodeRef parent, String name) + { + if(!nodeService.exists(parent)) + { + throw new AlfrescoRuntimeException("Missing category?"); + } + String uri = nodeService.getPrimaryParent(parent).getQName().getNamespaceURI(); + String validLocalName = QName.createValidLocalName(name); + NodeRef newCategory = nodeService.createNode(parent, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(uri, validLocalName), ContentModel.TYPE_CATEGORY).getChildRef(); + nodeService.setProperty(newCategory, ContentModel.PROP_NAME, name); + return newCategory; + } + + public NodeRef createRootCategory(StoreRef storeRef, QName aspectName, String name) + { + Set nodeRefs = getClassificationNodes(storeRef, aspectName); + if(nodeRefs.size() == 0) + { + throw new AlfrescoRuntimeException("Missing classification: "+aspectName); + } + NodeRef parent = nodeRefs.iterator().next(); + return createCategory(parent, name); + } + + public void deleteCategory(NodeRef nodeRef) + { + nodeService.deleteNode(nodeRef); + } + + public void deleteClassification(StoreRef storeRef, QName aspectName) + { + throw new UnsupportedOperationException(); + } + + + + + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryTest.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryTest.java new file mode 100644 index 0000000000..f8bf082963 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneCategoryTest.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Random; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.search.transaction.LuceneIndexLock; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class LuceneCategoryTest extends TestCase +{ + private ServiceRegistry serviceRegistry; + + static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + NodeService nodeService; + DictionaryService dictionaryService; + LuceneIndexLock luceneIndexLock; + private NodeRef rootNodeRef; + private NodeRef n1; + private NodeRef n2; + private NodeRef n3; + private NodeRef n4; + private NodeRef n6; + private NodeRef n5; + private NodeRef n7; + private NodeRef n8; + private NodeRef n9; + private NodeRef n10; + private NodeRef n11; + private NodeRef n12; + private NodeRef n13; + private NodeRef n14; + + private NodeRef catContainer; + private NodeRef catRoot; + private NodeRef catACBase; + private NodeRef catACOne; + private NodeRef catACTwo; + private NodeRef catACThree; + private FullTextSearchIndexer luceneFTS; + private DictionaryDAO dictionaryDAO; + private String TEST_NAMESPACE = "http://www.alfresco.org/test/lucenecategorytest"; + private QName regionCategorisationQName; + private QName assetClassCategorisationQName; + private QName investmentRegionCategorisationQName; + private QName marketingRegionCategorisationQName; + private NodeRef catRBase; + private NodeRef catROne; + private NodeRef catRTwo; + private NodeRef catRThree; + private SearchService searcher; + private LuceneIndexerAndSearcher indexerAndSearcher; + + private CategoryService categoryService; + + public LuceneCategoryTest() + { + super(); + } + + public LuceneCategoryTest(String arg0) + { + super(arg0); + } + + public void setUp() throws Exception + { + nodeService = (NodeService)ctx.getBean("dbNodeService"); + luceneIndexLock = (LuceneIndexLock)ctx.getBean("luceneIndexLock"); + dictionaryService = (DictionaryService)ctx.getBean("dictionaryService"); + luceneFTS = (FullTextSearchIndexer) ctx.getBean("LuceneFullTextSearchIndexer"); + dictionaryDAO = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + searcher = (SearchService) ctx.getBean("searchService"); + indexerAndSearcher = (LuceneIndexerAndSearcher) ctx.getBean("luceneIndexerAndSearcherFactory"); + categoryService = (CategoryService) ctx.getBean("categoryService"); + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + + createTestTypes(); + + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + + StoreRef storeRef = nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}one"), ContentModel.TYPE_CONTAINER).getChildRef(); + nodeService.setProperty(n1, QName.createQName("{namespace}property-1"), "value-1"); + n2 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}two"), ContentModel.TYPE_CONTAINER).getChildRef(); + nodeService.setProperty(n2, QName.createQName("{namespace}property-1"), "value-1"); + nodeService.setProperty(n2, QName.createQName("{namespace}property-2"), "value-2"); + n3 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}three"), ContentModel.TYPE_CONTAINER).getChildRef(); + n4 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}four"), ContentModel.TYPE_CONTAINER).getChildRef(); + n5 = nodeService.createNode(n1, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}five"), ContentModel.TYPE_CONTAINER).getChildRef(); + n6 = nodeService.createNode(n1, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}six"), ContentModel.TYPE_CONTAINER).getChildRef(); + n7 = nodeService.createNode(n2, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}seven"), ContentModel.TYPE_CONTAINER).getChildRef(); + n8 = nodeService.createNode(n2, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}eight-2"), ContentModel.TYPE_CONTAINER).getChildRef(); + n9 = nodeService.createNode(n5, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}nine"), ContentModel.TYPE_CONTAINER).getChildRef(); + n10 = nodeService.createNode(n5, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}ten"), ContentModel.TYPE_CONTAINER).getChildRef(); + n11 = nodeService.createNode(n5, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}eleven"), ContentModel.TYPE_CONTAINER).getChildRef(); + n12 = nodeService.createNode(n5, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}twelve"), ContentModel.TYPE_CONTAINER).getChildRef(); + n13 = nodeService.createNode(n12, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}thirteen"), ContentModel.TYPE_CONTAINER).getChildRef(); + n14 = nodeService.createNode(n13, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}fourteen"), ContentModel.TYPE_CONTAINER).getChildRef(); + + nodeService.addChild(rootNodeRef, n8, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}eight-0")); + nodeService.addChild(n1, n8, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}eight-1")); + nodeService.addChild(n2, n13, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}link")); + + nodeService.addChild(n1, n14, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}common")); + nodeService.addChild(n2, n14, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}common")); + nodeService.addChild(n5, n14, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}common")); + nodeService.addChild(n6, n14, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}common")); + nodeService.addChild(n12, n14, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}common")); + nodeService.addChild(n13, n14, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}common")); + + // Categories + + catContainer = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categoryContainer"), ContentModel.TYPE_CONTAINER).getChildRef(); + catRoot = nodeService.createNode(catContainer, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categoryRoot"), ContentModel.TYPE_CATEGORYROOT).getChildRef(); + + + + catRBase = nodeService.createNode(catRoot, ContentModel.ASSOC_CATEGORIES, QName.createQName(TEST_NAMESPACE, "Region"), ContentModel.TYPE_CATEGORY).getChildRef(); + catROne = nodeService.createNode(catRBase, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "Europe"), ContentModel.TYPE_CATEGORY).getChildRef(); + catRTwo = nodeService.createNode(catRBase, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "RestOfWorld"), ContentModel.TYPE_CATEGORY).getChildRef(); + catRThree = nodeService.createNode(catRTwo, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "US"), ContentModel.TYPE_CATEGORY).getChildRef(); + + nodeService.addChild(catRoot, catRBase, ContentModel.ASSOC_CATEGORIES, QName.createQName(TEST_NAMESPACE, "InvestmentRegion")); + nodeService.addChild(catRoot, catRBase, ContentModel.ASSOC_CATEGORIES, QName.createQName(TEST_NAMESPACE, "MarketingRegion")); + + + catACBase = nodeService.createNode(catRoot, ContentModel.ASSOC_CATEGORIES, QName.createQName(TEST_NAMESPACE, "AssetClass"), ContentModel.TYPE_CATEGORY).getChildRef(); + catACOne = nodeService.createNode(catACBase, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "Fixed"), ContentModel.TYPE_CATEGORY).getChildRef(); + catACTwo = nodeService.createNode(catACBase, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "Equity"), ContentModel.TYPE_CATEGORY).getChildRef(); + catACThree = nodeService.createNode(catACTwo, ContentModel.ASSOC_SUBCATEGORIES, QName.createQName(TEST_NAMESPACE, "SpecialEquity"), ContentModel.TYPE_CATEGORY).getChildRef(); + + + + nodeService.addAspect(n1, assetClassCategorisationQName, createMap("assetClass", catACBase)); + nodeService.addAspect(n1, regionCategorisationQName, createMap("region", catRBase)); + + nodeService.addAspect(n2, assetClassCategorisationQName, createMap("assetClass", catACOne)); + nodeService.addAspect(n3, assetClassCategorisationQName, createMap("assetClass", catACOne)); + nodeService.addAspect(n4, assetClassCategorisationQName, createMap("assetClass", catACOne)); + nodeService.addAspect(n5, assetClassCategorisationQName, createMap("assetClass", catACOne)); + nodeService.addAspect(n6, assetClassCategorisationQName, createMap("assetClass", catACOne)); + + nodeService.addAspect(n7, assetClassCategorisationQName, createMap("assetClass", catACTwo)); + nodeService.addAspect(n8, assetClassCategorisationQName, createMap("assetClass", catACTwo)); + nodeService.addAspect(n9, assetClassCategorisationQName, createMap("assetClass", catACTwo)); + nodeService.addAspect(n10, assetClassCategorisationQName, createMap("assetClass", catACTwo)); + nodeService.addAspect(n11, assetClassCategorisationQName, createMap("assetClass", catACTwo)); + + nodeService.addAspect(n12, assetClassCategorisationQName, createMap("assetClass", catACOne, catACTwo)); + nodeService.addAspect(n13, assetClassCategorisationQName, createMap("assetClass", catACOne, catACTwo, catACThree)); + nodeService.addAspect(n14, assetClassCategorisationQName, createMap("assetClass", catACOne, catACTwo)); + + nodeService.addAspect(n2, regionCategorisationQName, createMap("region", catROne)); + nodeService.addAspect(n3, regionCategorisationQName, createMap("region", catROne)); + nodeService.addAspect(n4, regionCategorisationQName, createMap("region", catRTwo)); + nodeService.addAspect(n5, regionCategorisationQName, createMap("region", catRTwo)); + + nodeService.addAspect(n5, investmentRegionCategorisationQName, createMap("investmentRegion", catRBase)); + nodeService.addAspect(n5, marketingRegionCategorisationQName, createMap("marketingRegion", catRBase)); + nodeService.addAspect(n6, investmentRegionCategorisationQName, createMap("investmentRegion", catRBase)); + nodeService.addAspect(n7, investmentRegionCategorisationQName, createMap("investmentRegion", catRBase)); + nodeService.addAspect(n8, investmentRegionCategorisationQName, createMap("investmentRegion", catRBase)); + nodeService.addAspect(n9, investmentRegionCategorisationQName, createMap("investmentRegion", catRBase)); + nodeService.addAspect(n10, marketingRegionCategorisationQName, createMap("marketingRegion", catRBase)); + nodeService.addAspect(n11, marketingRegionCategorisationQName, createMap("marketingRegion", catRBase)); + nodeService.addAspect(n12, marketingRegionCategorisationQName, createMap("marketingRegion", catRBase)); + nodeService.addAspect(n13, marketingRegionCategorisationQName, createMap("marketingRegion", catRBase)); + nodeService.addAspect(n14, marketingRegionCategorisationQName, createMap("marketingRegion", catRBase)); + + tx.commit(); + } + + private HashMap createMap(String name, NodeRef[] nodeRefs) + { + HashMap map = new HashMap(); + Serializable value = (Serializable) Arrays.asList(nodeRefs); + map.put(QName.createQName(TEST_NAMESPACE, name), value); + return map; + } + + private HashMap createMap(String name, NodeRef nodeRef) + { + return createMap(name, new NodeRef[]{nodeRef}); + } + + private HashMap createMap(String name, NodeRef nodeRef1, NodeRef nodeRef2) + { + return createMap(name, new NodeRef[]{nodeRef1, nodeRef2}); + } + + private HashMap createMap(String name, NodeRef nodeRef1, NodeRef nodeRef2, NodeRef nodeRef3) + { + return createMap(name, new NodeRef[]{nodeRef1, nodeRef2, nodeRef3}); + } + + private void createTestTypes() + { + M2Model model = M2Model.createModel("test:lucenecategory"); + model.createNamespace(TEST_NAMESPACE, "test"); + model.createImport(NamespaceService.DICTIONARY_MODEL_1_0_URI, NamespaceService.DICTIONARY_MODEL_PREFIX); + model.createImport(NamespaceService.CONTENT_MODEL_1_0_URI, NamespaceService.CONTENT_MODEL_PREFIX); + + regionCategorisationQName = QName.createQName(TEST_NAMESPACE, "Region"); + M2Aspect generalCategorisation = model.createAspect("test:" + regionCategorisationQName.getLocalName()); + generalCategorisation.setParentName("cm:" + ContentModel.ASPECT_CLASSIFIABLE.getLocalName()); + M2Property genCatProp = generalCategorisation.createProperty("test:region"); + genCatProp.setIndexed(true); + genCatProp.setIndexedAtomically(true); + genCatProp.setMandatory(true); + genCatProp.setMultiValued(true); + genCatProp.setStoredInIndex(true); + genCatProp.setTokenisedInIndex(true); + genCatProp.setType("d:" + DataTypeDefinition.CATEGORY.getLocalName()); + + assetClassCategorisationQName = QName.createQName(TEST_NAMESPACE, "AssetClass"); + M2Aspect assetClassCategorisation = model.createAspect("test:" + assetClassCategorisationQName.getLocalName()); + assetClassCategorisation.setParentName("cm:" + ContentModel.ASPECT_CLASSIFIABLE.getLocalName()); + M2Property acProp = assetClassCategorisation.createProperty("test:assetClass"); + acProp.setIndexed(true); + acProp.setIndexedAtomically(true); + acProp.setMandatory(true); + acProp.setMultiValued(true); + acProp.setStoredInIndex(true); + acProp.setTokenisedInIndex(true); + acProp.setType("d:" + DataTypeDefinition.CATEGORY.getLocalName()); + + investmentRegionCategorisationQName = QName.createQName(TEST_NAMESPACE, "InvestmentRegion"); + M2Aspect investmentRegionCategorisation = model.createAspect("test:" + investmentRegionCategorisationQName.getLocalName()); + investmentRegionCategorisation.setParentName("cm:" + ContentModel.ASPECT_CLASSIFIABLE.getLocalName()); + M2Property irProp = investmentRegionCategorisation.createProperty("test:investmentRegion"); + irProp.setIndexed(true); + irProp.setIndexedAtomically(true); + irProp.setMandatory(true); + irProp.setMultiValued(true); + irProp.setStoredInIndex(true); + irProp.setTokenisedInIndex(true); + irProp.setType("d:" + DataTypeDefinition.CATEGORY.getLocalName()); + + marketingRegionCategorisationQName = QName.createQName(TEST_NAMESPACE, "MarketingRegion"); + M2Aspect marketingRegionCategorisation = model.createAspect("test:" + marketingRegionCategorisationQName.getLocalName()); + marketingRegionCategorisation.setParentName("cm:" + ContentModel.ASPECT_CLASSIFIABLE.getLocalName()); + M2Property mrProp = marketingRegionCategorisation.createProperty("test:marketingRegion"); + mrProp.setIndexed(true); + mrProp.setIndexedAtomically(true); + mrProp.setMandatory(true); + mrProp.setMultiValued(true); + mrProp.setStoredInIndex(true); + mrProp.setTokenisedInIndex(true); + mrProp.setType("d:" + DataTypeDefinition.CATEGORY.getLocalName()); + + dictionaryDAO.putModel(model); + } + + private void buildBaseIndex() + { + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + System.currentTimeMillis() + "_" + (new Random().nextInt()), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + //indexer.clearIndex(); + indexer.createNode(new ChildAssociationRef(null, null, null, rootNodeRef)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName.createQName("{namespace}one"), n1)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName.createQName("{namespace}two"), n2)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName.createQName("{namespace}three"), n3)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName.createQName("{namespace}four"), n4)); + + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName.createQName("{namespace}categoryContainer"), catContainer)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, catContainer, QName.createQName("{cat}categoryRoot"), catRoot)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, catRoot, QName.createQName(TEST_NAMESPACE, "AssetClass"), catACBase)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, catACBase, QName.createQName(TEST_NAMESPACE, "Fixed"), catACOne)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, catACBase, QName.createQName(TEST_NAMESPACE, "Equity"), catACTwo)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, catACTwo, QName.createQName(TEST_NAMESPACE, "SpecialEquity"), catACThree)); + + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, catRoot, QName.createQName(TEST_NAMESPACE, "Region"), catRBase)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, catRBase, QName.createQName(TEST_NAMESPACE, "Europe"), catROne)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, catRBase, QName.createQName(TEST_NAMESPACE, "RestOfWorld"), catRTwo)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, catRTwo, QName.createQName(TEST_NAMESPACE, "US"), catRThree)); + + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n1, QName.createQName("{namespace}five"), n5)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n1, QName.createQName("{namespace}six"), n6)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n2, QName.createQName("{namespace}seven"), n7)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n2, QName.createQName("{namespace}eight"), n8)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n5, QName.createQName("{namespace}nine"), n9)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n5, QName.createQName("{namespace}ten"), n10)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n5, QName.createQName("{namespace}eleven"), n11)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n5, QName.createQName("{namespace}twelve"), n12)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n12, QName.createQName("{namespace}thirteen"), n13)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CATEGORIES, n13, QName.createQName("{namespace}fourteen"), n14)); + indexer.prepare(); + indexer.commit(); + } + + + public void testMulti() throws Exception + { + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("")); + ResultSet results; + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*\" AND (PATH:\"/test:AssetClass/test:Equity/member\" PATH:\"/test:MarketingRegion/member\")", null, null); + //printPaths(results); + assertEquals(9, results.length()); + results.close(); + tx.rollback(); + } + + public void testBasic() throws Exception + { + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("")); + ResultSet results; + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:MarketingRegion\"", null, null); + //printPaths(results); + assertEquals(1, results.length()); + results.close(); + + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:MarketingRegion//member\"", null, null); + //printPaths(results); + assertEquals(6, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass/member\" ", null, null); + assertEquals(1, results.length()); + results.close(); + + + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass/test:Fixed\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass/test:Equity\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Fixed\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:*\"", null, null); + assertEquals(2, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass//test:*\"", null, null); + assertEquals(3, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Fixed/member\"", null, null); + //printPaths(results); + assertEquals(8, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity/member\"", null, null); + //printPaths(results); + assertEquals(8, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity/test:SpecialEquity/member//.\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity/test:SpecialEquity/member//*\"", null, null); + assertEquals(0, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity/test:SpecialEquity/member\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "+PATH:\"/test:AssetClass/test:Equity/member\" AND +PATH:\"/test:AssetClass/test:Fixed/member\"", null, null); + //printPaths(results); + assertEquals(3, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity/member\" PATH:\"/test:AssetClass/test:Fixed/member\"", null, null); + //printPaths(results); + assertEquals(13, results.length()); + results.close(); + + // Region + + assertEquals(4, nodeService.getChildAssocs(catRoot).size()); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:Region\"", null, null); + //printPaths(results); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:Region/member\"", null, null); + //printPaths(results); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:Region/test:Europe/member\"", null, null); + //printPaths(results); + assertEquals(2, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:Region/test:RestOfWorld/member\"", null, null); + //printPaths(results); + assertEquals(2, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:Region//member\"", null, null); + //printPaths(results); + assertEquals(5, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:InvestmentRegion//member\"", null, null); + //printPaths(results); + assertEquals(5, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:MarketingRegion//member\"", null, null); + //printPaths(results); + assertEquals(6, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "+PATH:\"/test:AssetClass/test:Fixed/member\" AND +PATH:\"/test:Region/test:Europe/member\"", null, null); + //printPaths(results); + assertEquals(2, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "+PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass/test:Fixed/member\" AND +PATH:\"/cm:categoryContainer/cm:categoryRoot/test:Region/test:Europe/member\"", null, null); + //printPaths(results); + assertEquals(2, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/test:AssetClass/test:Equity/member\" PATH:\"/test:MarketingRegion/member\"", null, null); + //printPaths(results); + assertEquals(9, results.length()); + results.close(); + tx.rollback(); + } + + public void testCategoryServiceImpl() throws Exception + { + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("")); + + ResultSet + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass/*\" ", null, null); + assertEquals(3, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/cm:categoryContainer/cm:categoryRoot/test:AssetClass/member\" ", null, null); + assertEquals(1, results.length()); + results.close(); + + LuceneCategoryServiceImpl impl = new LuceneCategoryServiceImpl(); + impl.setNodeService(nodeService); + impl.setNamespacePrefixResolver(getNamespacePrefixReolsver("")); + impl.setIndexerAndSearcher(indexerAndSearcher); + impl.setDictionaryService(dictionaryService); + + Collection + result = impl.getChildren(catACBase , CategoryService.Mode.MEMBERS, CategoryService.Depth.IMMEDIATE); + assertEquals(1, result.size()); + + + result = impl.getChildren(catACBase , CategoryService.Mode.ALL, CategoryService.Depth.IMMEDIATE); + assertEquals(3, result.size()); + + + result = impl.getChildren(catACBase , CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.IMMEDIATE); + assertEquals(2, result.size()); + + + result = impl.getChildren(catACBase , CategoryService.Mode.MEMBERS, CategoryService.Depth.ANY); + assertEquals(18, result.size()); + + + result = impl.getChildren(catACBase , CategoryService.Mode.ALL, CategoryService.Depth.ANY); + assertEquals(21, result.size()); + + + result = impl.getChildren(catACBase , CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.ANY); + assertEquals(3, result.size()); + + + result = impl.getClassifications(rootNodeRef.getStoreRef()); + assertEquals(4, result.size()); + + + result = impl.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.IMMEDIATE); + assertEquals(2, result.size()); + + + Collection aspects = impl.getClassificationAspects(); + assertEquals(6, aspects.size()); + + tx.rollback(); + } + + private NamespacePrefixResolver getNamespacePrefixReolsver(String defaultURI) + { + DynamicNamespacePrefixResolver nspr = new DynamicNamespacePrefixResolver(null); + nspr.registerNamespace(NamespaceService.CONTENT_MODEL_PREFIX, NamespaceService.CONTENT_MODEL_1_0_URI); + nspr.registerNamespace("namespace", "namespace"); + nspr.registerNamespace("test", TEST_NAMESPACE); + nspr.registerNamespace(NamespaceService.DEFAULT_PREFIX, defaultURI); + return nspr; + } + + public void testCategoryService() throws Exception + { + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + buildBaseIndex(); + assertEquals(1, categoryService.getChildren(catACBase , CategoryService.Mode.MEMBERS, CategoryService.Depth.IMMEDIATE).size()); + assertEquals(2, categoryService.getChildren(catACBase , CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.IMMEDIATE).size()); + assertEquals(3, categoryService.getChildren(catACBase , CategoryService.Mode.ALL, CategoryService.Depth.IMMEDIATE).size()); + assertEquals(18, categoryService.getChildren(catACBase , CategoryService.Mode.MEMBERS, CategoryService.Depth.ANY).size()); + assertEquals(3, categoryService.getChildren(catACBase , CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.ANY).size()); + assertEquals(21, categoryService.getChildren(catACBase , CategoryService.Mode.ALL, CategoryService.Depth.ANY).size()); + assertEquals(4, categoryService.getClassifications(rootNodeRef.getStoreRef()).size()); + assertEquals(2, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.IMMEDIATE).size()); + assertEquals(3, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.ANY).size()); + assertEquals(6, categoryService.getClassificationAspects().size()); + assertEquals(2, categoryService.getRootCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass")).size()); + + NodeRef newRoot = categoryService.createRootCategory(rootNodeRef.getStoreRef(),QName.createQName(TEST_NAMESPACE, "AssetClass"), "Fruit"); + tx.commit(); + tx = transactionService.getUserTransaction(); + tx.begin(); + assertEquals(3, categoryService.getRootCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass")).size()); + assertEquals(3, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.IMMEDIATE).size()); + assertEquals(4, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.ANY).size()); + + NodeRef newCat = categoryService.createCategory(newRoot, "Banana"); + tx.commit(); + tx = transactionService.getUserTransaction(); + tx.begin(); + assertEquals(3, categoryService.getRootCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass")).size()); + assertEquals(3, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.IMMEDIATE).size()); + assertEquals(5, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.ANY).size()); + + categoryService.deleteCategory(newCat); + tx.commit(); + tx = transactionService.getUserTransaction(); + tx.begin(); + assertEquals(3, categoryService.getRootCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass")).size()); + assertEquals(3, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.IMMEDIATE).size()); + assertEquals(4, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.ANY).size()); + + categoryService.deleteCategory(newRoot); + tx.commit(); + tx = transactionService.getUserTransaction(); + tx.begin(); + assertEquals(2, categoryService.getRootCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass")).size()); + assertEquals(2, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.IMMEDIATE).size()); + assertEquals(3, categoryService.getCategories(rootNodeRef.getStoreRef(), QName.createQName(TEST_NAMESPACE, "AssetClass"), CategoryService.Depth.ANY).size()); + + + tx.rollback(); + } + + private int getTotalScore(ResultSet results) + { + int totalScore = 0; + for(ResultSetRow row: results) + { + totalScore += row.getScore(); + } + return totalScore; + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneConfig.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneConfig.java new file mode 100644 index 0000000000..fee4da57cb --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +public interface LuceneConfig +{ + + public String getIndexRootLocation(); + + public int getIndexerBatchSize(); + + public int getIndexerMaxMergeDocs(); + + public int getIndexerMergeFactor(); + + public int getIndexerMinMergeDocs(); + + public String getLockDirectory(); + + public int getQueryMaxClauses(); + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexBackupComponentTest.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexBackupComponentTest.java new file mode 100644 index 0000000000..615c5ad050 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexBackupComponentTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.File; + +import org.alfresco.repo.search.impl.lucene.LuceneIndexerAndSearcherFactory.LuceneIndexBackupComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.TempFileProvider; +import org.springframework.context.ApplicationContext; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.repo.search.impl.lucene.LuceneIndexerAndSearcherFactory.LuceneIndexBackupComponent + * + * @author Derek Hulley + */ +public class LuceneIndexBackupComponentTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private LuceneIndexBackupComponent backupComponent; + private File tempTargetDir; + + private AuthenticationComponent authenticationComponent; + + @Override + public void setUp() throws Exception + { + TransactionService transactionService = (TransactionService) ctx.getBean("transactionComponent"); + NodeService nodeService = (NodeService) ctx.getBean("NodeService"); + LuceneIndexerAndSearcherFactory factory = (LuceneIndexerAndSearcherFactory) ctx.getBean("luceneIndexerAndSearcherFactory"); + + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + this.authenticationComponent.setSystemUserAsCurrentUser(); + + tempTargetDir = new File(TempFileProvider.getTempDir(), getName()); + tempTargetDir.mkdir(); + + backupComponent = new LuceneIndexBackupComponent(); + backupComponent.setTransactionService(transactionService); + backupComponent.setFactory(factory); + backupComponent.setNodeService(nodeService); + backupComponent.setTargetLocation(tempTargetDir.toString()); + } + + @Override + protected void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + super.tearDown(); + } + + public void testBackup() + { + backupComponent.backup(); + + // make sure that the target directory was created + assertTrue("Target location doesn't exist", tempTargetDir.exists()); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexException.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexException.java new file mode 100644 index 0000000000..7fafdd4c26 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.alfresco.repo.search.IndexerException; + +/** + * Exceptions relating to indexing within the lucene implementation + * + * @author andyh + * + */ +public class LuceneIndexException extends IndexerException +{ + + /** + * + */ + private static final long serialVersionUID = 3688505480817422645L; + + public LuceneIndexException(String message, Throwable cause) + { + super(message, cause); + } + + public LuceneIndexException(String message) + { + super(message); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexer.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexer.java new file mode 100644 index 0000000000..763ea20996 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexer.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.util.Set; + +import org.alfresco.repo.search.Indexer; +import org.alfresco.repo.search.impl.lucene.fts.FTSIndexerAware; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; + +/** + * @author Andy Hind + */ +public interface LuceneIndexer extends Indexer, Lockable +{ + + public void commit(); + public void rollback(); + public int prepare(); + public boolean isModified(); + public void setNodeService(NodeService nodeService); + public void setDictionaryService(DictionaryService dictionaryService); + public void setLuceneFullTextSearchIndexer(FullTextSearchIndexer luceneFullTextSearchIndexer); + + public void updateFullTextSearch(int size); + public void registerCallBack(FTSIndexerAware indexer); + + public String getDeltaId(); + public void flushPending() throws LuceneIndexException; + public Set getDeletions(); +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcher.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcher.java new file mode 100644 index 0000000000..14627c3092 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcher.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.alfresco.repo.search.IndexerAndSearcher; +import org.alfresco.repo.search.IndexerException; + +public interface LuceneIndexerAndSearcher extends IndexerAndSearcher, LuceneConfig +{ + public int prepare() throws IndexerException; + public void commit() throws IndexerException; + public void rollback(); +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcherFactory.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcherFactory.java new file mode 100644 index 0000000000..1baca6491d --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerAndSearcherFactory.java @@ -0,0 +1,1072 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.search.IndexerException; +import org.alfresco.repo.search.QueryRegisterComponent; +import org.alfresco.repo.search.SearcherException; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.search.transaction.LuceneIndexLock; +import org.alfresco.repo.search.transaction.SimpleTransaction; +import org.alfresco.repo.search.transaction.SimpleTransactionManager; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.GUID; +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.lucene.search.BooleanQuery; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * This class is resource manager LuceneIndexers and LuceneSearchers. + * + * It supports two phase commit inside XA transactions and outside transactions + * it provides thread local transaction support. + * + * TODO: Provide pluggable support for a transaction manager TODO: Integrate + * with Spring transactions + * + * @author andyh + * + */ + +public class LuceneIndexerAndSearcherFactory implements LuceneIndexerAndSearcher, XAResource +{ + private DictionaryService dictionaryService; + + private NamespaceService nameSpaceService; + + private int queryMaxClauses; + + private int indexerBatchSize; + + private int indexerMinMergeDocs; + + private int indexerMergeFactor; + + private int indexerMaxMergeDocs; + + private String lockDirectory; + + /** + * A map of active global transactions . It contains all the indexers a + * transaction has used, with at most one indexer for each store within a + * transaction + */ + + private static Map> activeIndexersInGlobalTx = new HashMap>(); + + /** + * Suspended global transactions. + */ + private static Map> suspendedIndexersInGlobalTx = new HashMap>(); + + /** + * Thread local indexers - used outside a global transaction + */ + + private static ThreadLocal> threadLocalIndexers = new ThreadLocal>(); + + /** + * The dafault timeout for transactions TODO: Respect this + */ + + private int timeout = DEFAULT_TIMEOUT; + + /** + * Default time out value set to 10 minutes. + */ + private static final int DEFAULT_TIMEOUT = 600000; + + /** + * The node service we use to get information about nodes + */ + + private NodeService nodeService; + + private LuceneIndexLock luceneIndexLock; + + private FullTextSearchIndexer luceneFullTextSearchIndexer; + + private String indexRootLocation; + + private ContentService contentService; + + private QueryRegisterComponent queryRegister; + + private int indexerMaxFieldLength; + + /** + * Private constructor for the singleton TODO: FIt in with IOC + */ + + public LuceneIndexerAndSearcherFactory() + { + super(); + } + + /** + * Setter for getting the node service via IOC Used in the Spring container + * + * @param nodeService + */ + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNameSpaceService(NamespaceService nameSpaceService) + { + this.nameSpaceService = nameSpaceService; + } + + public void setLuceneIndexLock(LuceneIndexLock luceneIndexLock) + { + this.luceneIndexLock = luceneIndexLock; + } + + public void setLuceneFullTextSearchIndexer(FullTextSearchIndexer luceneFullTextSearchIndexer) + { + this.luceneFullTextSearchIndexer = luceneFullTextSearchIndexer; + } + + public void setIndexRootLocation(String indexRootLocation) + { + this.indexRootLocation = indexRootLocation; + } + + public void setQueryRegister(QueryRegisterComponent queryRegister) + { + this.queryRegister = queryRegister; + } + + /** + * Check if we are in a global transactoin according to the transaction + * manager + * + * @return + */ + + private boolean inGlobalTransaction() + { + try + { + return SimpleTransactionManager.getInstance().getTransaction() != null; + } + catch (SystemException e) + { + return false; + } + } + + /** + * Get the local transaction - may be null oif we are outside a transaction. + * + * @return + * @throws IndexerException + */ + private SimpleTransaction getTransaction() throws IndexerException + { + try + { + return SimpleTransactionManager.getInstance().getTransaction(); + } + catch (SystemException e) + { + throw new IndexerException("Failed to get transaction", e); + } + } + + /** + * Get an indexer for the store to use in the current transaction for this + * thread of control. + * + * @param storeRef - + * the id of the store + */ + public LuceneIndexer getIndexer(StoreRef storeRef) throws IndexerException + { + // register to receive txn callbacks + // TODO: make this conditional on whether the XA stuff is being used + // directly on not + AlfrescoTransactionSupport.bindLucene(this); + + if (inGlobalTransaction()) + { + SimpleTransaction tx = getTransaction(); + // Only find indexers in the active list + Map indexers = activeIndexersInGlobalTx.get(tx); + if (indexers == null) + { + if (suspendedIndexersInGlobalTx.containsKey(tx)) + { + throw new IndexerException("Trying to obtain an index for a suspended transaction."); + } + indexers = new HashMap(); + activeIndexersInGlobalTx.put(tx, indexers); + try + { + tx.enlistResource(this); + } + // TODO: what to do in each case? + catch (IllegalStateException e) + { + throw new IndexerException("", e); + } + catch (RollbackException e) + { + throw new IndexerException("", e); + } + catch (SystemException e) + { + throw new IndexerException("", e); + } + } + LuceneIndexer indexer = indexers.get(storeRef); + if (indexer == null) + { + indexer = createIndexer(storeRef, getTransactionId(tx, storeRef)); + indexers.put(storeRef, indexer); + } + return indexer; + } + else + // A thread local transaction + { + return getThreadLocalIndexer(storeRef); + } + + } + + private LuceneIndexer getThreadLocalIndexer(StoreRef storeRef) + { + Map indexers = threadLocalIndexers.get(); + if (indexers == null) + { + indexers = new HashMap(); + threadLocalIndexers.set(indexers); + } + LuceneIndexer indexer = indexers.get(storeRef); + if (indexer == null) + { + indexer = createIndexer(storeRef, GUID.generate()); + indexers.put(storeRef, indexer); + } + return indexer; + } + + /** + * Get the transaction identifier uised to store it in the transaction map. + * + * @param tx + * @return + */ + private static String getTransactionId(Transaction tx, StoreRef storeRef) + { + if (tx instanceof SimpleTransaction) + { + SimpleTransaction simpleTx = (SimpleTransaction) tx; + return simpleTx.getGUID(); + } + else + { + Map indexers = threadLocalIndexers.get(); + if (indexers != null) + { + LuceneIndexer indexer = indexers.get(storeRef); + if (indexer != null) + { + return indexer.getDeltaId(); + } + } + return null; + } + } + + /** + * Encapsulate creating an indexer + * + * @param storeRef + * @param deltaId + * @return + */ + private LuceneIndexerImpl createIndexer(StoreRef storeRef, String deltaId) + { + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(storeRef, deltaId, this); + indexer.setNodeService(nodeService); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setLuceneFullTextSearchIndexer(luceneFullTextSearchIndexer); + indexer.setContentService(contentService); + return indexer; + } + + /** + * Encapsulate creating a searcher over the main index + */ + public LuceneSearcher getSearcher(StoreRef storeRef, boolean searchDelta) throws SearcherException + { + String deltaId = null; + LuceneIndexer indexer = null; + if (searchDelta) + { + deltaId = getTransactionId(getTransaction(), storeRef); + if (deltaId != null) + { + indexer = getIndexer(storeRef); + } + } + LuceneSearcher searcher = getSearcher(storeRef, indexer); + return searcher; + } + + /** + * Get a searcher over the index and the current delta + * + * @param storeRef + * @param deltaId + * @return + * @throws SearcherException + */ + private LuceneSearcher getSearcher(StoreRef storeRef, LuceneIndexer indexer) throws SearcherException + { + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(storeRef, indexer, this); + searcher.setNamespacePrefixResolver(nameSpaceService); + searcher.setLuceneIndexLock(luceneIndexLock); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setQueryRegister(queryRegister); + return searcher; + } + + /* + * XAResource implementation + */ + + public void commit(Xid xid, boolean onePhase) throws XAException + { + try + { + // TODO: Should be remembering overall state + // TODO: Keep track of prepare responses + Map indexers = activeIndexersInGlobalTx.get(xid); + if (indexers == null) + { + if (suspendedIndexersInGlobalTx.containsKey(xid)) + { + throw new XAException("Trying to commit indexes for a suspended transaction."); + } + else + { + // nothing to do + return; + } + } + + if (onePhase) + { + if (indexers.size() == 0) + { + return; + } + else if (indexers.size() == 1) + { + for (LuceneIndexer indexer : indexers.values()) + { + indexer.commit(); + } + return; + } + else + { + throw new XAException("Trying to do one phase commit on more than one index"); + } + } + else + // two phase + { + for (LuceneIndexer indexer : indexers.values()) + { + indexer.commit(); + } + return; + } + } finally + { + activeIndexersInGlobalTx.remove(xid); + } + } + + public void end(Xid xid, int flag) throws XAException + { + Map indexers = activeIndexersInGlobalTx.get(xid); + if (indexers == null) + { + if (suspendedIndexersInGlobalTx.containsKey(xid)) + { + throw new XAException("Trying to commit indexes for a suspended transaction."); + } + else + { + // nothing to do + return; + } + } + if (flag == XAResource.TMSUSPEND) + { + activeIndexersInGlobalTx.remove(xid); + suspendedIndexersInGlobalTx.put(xid, indexers); + } + else if (flag == TMFAIL) + { + activeIndexersInGlobalTx.remove(xid); + suspendedIndexersInGlobalTx.remove(xid); + } + else if (flag == TMSUCCESS) + { + activeIndexersInGlobalTx.remove(xid); + } + } + + public void forget(Xid xid) throws XAException + { + activeIndexersInGlobalTx.remove(xid); + suspendedIndexersInGlobalTx.remove(xid); + } + + public int getTransactionTimeout() throws XAException + { + return timeout; + } + + public boolean isSameRM(XAResource xar) throws XAException + { + return (xar instanceof LuceneIndexerAndSearcherFactory); + } + + public int prepare(Xid xid) throws XAException + { + // TODO: Track state OK, ReadOnly, Exception (=> rolled back?) + Map indexers = activeIndexersInGlobalTx.get(xid); + if (indexers == null) + { + if (suspendedIndexersInGlobalTx.containsKey(xid)) + { + throw new XAException("Trying to commit indexes for a suspended transaction."); + } + else + { + // nothing to do + return XAResource.XA_OK; + } + } + boolean isPrepared = true; + boolean isModified = false; + for (LuceneIndexer indexer : indexers.values()) + { + try + { + isModified |= indexer.isModified(); + indexer.prepare(); + } + catch (IndexerException e) + { + isPrepared = false; + } + } + if (isPrepared) + { + if (isModified) + { + return XAResource.XA_OK; + } + else + { + return XAResource.XA_RDONLY; + } + } + else + { + throw new XAException("Failed to prepare: requires rollback"); + } + } + + public Xid[] recover(int arg0) throws XAException + { + // We can not rely on being able to recover at the moment + // Avoiding for performance benefits at the moment + // Assume roll back and no recovery - in the worst case we get an unused + // delta + // This should be there to avoid recovery of partial commits. + // It is difficult to see how we can mandate the same conditions. + return new Xid[0]; + } + + public void rollback(Xid xid) throws XAException + { + // TODO: What to do if all do not roll back? + try + { + Map indexers = activeIndexersInGlobalTx.get(xid); + if (indexers == null) + { + if (suspendedIndexersInGlobalTx.containsKey(xid)) + { + throw new XAException("Trying to commit indexes for a suspended transaction."); + } + else + { + // nothing to do + return; + } + } + for (LuceneIndexer indexer : indexers.values()) + { + indexer.rollback(); + } + } finally + { + activeIndexersInGlobalTx.remove(xid); + } + } + + public boolean setTransactionTimeout(int timeout) throws XAException + { + this.timeout = timeout; + return true; + } + + public void start(Xid xid, int flag) throws XAException + { + Map active = activeIndexersInGlobalTx.get(xid); + Map suspended = suspendedIndexersInGlobalTx.get(xid); + if (flag == XAResource.TMJOIN) + { + // must be active + if ((active != null) && (suspended == null)) + { + return; + } + else + { + throw new XAException("Trying to rejoin transaction in an invalid state"); + } + + } + else if (flag == XAResource.TMRESUME) + { + // must be suspended + if ((active == null) && (suspended != null)) + { + suspendedIndexersInGlobalTx.remove(xid); + activeIndexersInGlobalTx.put(xid, suspended); + return; + } + else + { + throw new XAException("Trying to rejoin transaction in an invalid state"); + } + + } + else if (flag == XAResource.TMNOFLAGS) + { + if ((active == null) && (suspended == null)) + { + return; + } + else + { + throw new XAException("Trying to start an existing or suspended transaction"); + } + } + else + { + throw new XAException("Unkown flags for start " + flag); + } + + } + + /* + * Thread local support for transactions + */ + + /** + * Commit the transaction + */ + + public void commit() throws IndexerException + { + try + { + Map indexers = threadLocalIndexers.get(); + if (indexers != null) + { + for (LuceneIndexer indexer : indexers.values()) + { + try + { + indexer.commit(); + } + catch (IndexerException e) + { + rollback(); + throw e; + } + } + } + } finally + { + if (threadLocalIndexers.get() != null) + { + threadLocalIndexers.get().clear(); + threadLocalIndexers.set(null); + } + } + } + + /** + * Prepare the transaction TODO: Store prepare results + * + * @return + */ + public int prepare() throws IndexerException + { + boolean isPrepared = true; + boolean isModified = false; + Map indexers = threadLocalIndexers.get(); + if (indexers != null) + { + for (LuceneIndexer indexer : indexers.values()) + { + try + { + isModified |= indexer.isModified(); + indexer.prepare(); + } + catch (IndexerException e) + { + isPrepared = false; + throw new IndexerException("Failed to prepare: requires rollback", e); + } + } + } + if (isPrepared) + { + if (isModified) + { + return XAResource.XA_OK; + } + else + { + return XAResource.XA_RDONLY; + } + } + else + { + throw new IndexerException("Failed to prepare: requires rollback"); + } + } + + /** + * Roll back the transaction + */ + public void rollback() + { + Map indexers = threadLocalIndexers.get(); + + if (indexers != null) + { + for (LuceneIndexer indexer : indexers.values()) + { + try + { + indexer.rollback(); + } + catch (IndexerException e) + { + + } + } + } + + if (threadLocalIndexers.get() != null) + { + threadLocalIndexers.get().clear(); + threadLocalIndexers.set(null); + } + + } + + public void flush() + { + // TODO: Needs fixing if we expose the indexer in JTA + Map indexers = threadLocalIndexers.get(); + + if (indexers != null) + { + for (LuceneIndexer indexer : indexers.values()) + { + indexer.flushPending(); + } + } + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public String getIndexRootLocation() + { + return indexRootLocation; + } + + public int getIndexerBatchSize() + { + return indexerBatchSize; + } + + public void setIndexerBatchSize(int indexerBatchSize) + { + this.indexerBatchSize = indexerBatchSize; + } + + public int getIndexerMaxMergeDocs() + { + return indexerMaxMergeDocs; + } + + public void setIndexerMaxMergeDocs(int indexerMaxMergeDocs) + { + this.indexerMaxMergeDocs = indexerMaxMergeDocs; + } + + public int getIndexerMergeFactor() + { + return indexerMergeFactor; + } + + public void setIndexerMergeFactor(int indexerMergeFactor) + { + this.indexerMergeFactor = indexerMergeFactor; + } + + public int getIndexerMinMergeDocs() + { + return indexerMinMergeDocs; + } + + public void setIndexerMinMergeDocs(int indexerMinMergeDocs) + { + this.indexerMinMergeDocs = indexerMinMergeDocs; + } + + public String getLockDirectory() + { + return lockDirectory; + } + + public void setLockDirectory(String lockDirectory) + { + this.lockDirectory = lockDirectory; + // Set the lucene lock file via System property + // org.apache.lucene.lockdir + System.setProperty("org.apache.lucene.lockdir", lockDirectory); + // Make sure the lock directory exists + File lockDir = new File(lockDirectory); + if (!lockDir.exists()) + { + lockDir.mkdirs(); + } + // clean out any existing locks when we start up + + File[] children = lockDir.listFiles(); + if (children != null) + { + for (int i = 0; i < children.length; i++) + { + File child = children[i]; + if (child.isFile()) + { + if (child.exists() && !child.delete() && child.exists()) + { + throw new IllegalStateException("Failed to delete " + child); + } + } + } + } + } + + public int getQueryMaxClauses() + { + return queryMaxClauses; + } + + public void setQueryMaxClauses(int queryMaxClauses) + { + this.queryMaxClauses = queryMaxClauses; + BooleanQuery.setMaxClauseCount(this.queryMaxClauses); + } + + public int getIndexerMaxFieldLength() + { + return indexerMaxFieldLength; + } + + public void setIndexerMaxFieldLength(int indexerMaxFieldLength) + { + this.indexerMaxFieldLength = indexerMaxFieldLength; + System.setProperty("org.apache.lucene.maxFieldLength", "" + indexerMaxFieldLength); + } + + /** + * This component is able to safely perform backups of the Lucene indexes while + * the server is running. + *

    + * It can be run directly by calling the {@link #backup() } method, but the convenience + * {@link LuceneIndexBackupJob} can be used to call it as well. + * + * @author Derek Hulley + */ + public static class LuceneIndexBackupComponent + { + private static Log logger = LogFactory.getLog(LuceneIndexerAndSearcherFactory.class); + + private TransactionService transactionService; + private LuceneIndexerAndSearcherFactory factory; + private NodeService nodeService; + private String targetLocation; + + public LuceneIndexBackupComponent() + { + } + + /** + * Provides transactions in which to perform the work + * + * @param transactionService + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the Lucene index factory that will be used to control the index locks + * + * @param factory the index factory + */ + public void setFactory(LuceneIndexerAndSearcherFactory factory) + { + this.factory = factory; + } + + /** + * Used to retrieve the stores + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the directory to which the backup will be copied + * + * @param targetLocation the backup directory + */ + public void setTargetLocation(String targetLocation) + { + this.targetLocation = targetLocation; + } + + /** + * Backup the Lucene indexes + */ + public void backup() + { + TransactionWork backupWork = new TransactionWork() + { + public Object doWork() throws Exception + { + backupImpl(); + return null; + } + }; + TransactionUtil.executeInUserTransaction(transactionService, backupWork); + } + + private void backupImpl() + { + // create the location to copy to + File targetDir = new File(targetLocation); + if (targetDir.exists() && !targetDir.isDirectory()) + { + throw new AlfrescoRuntimeException("Target location is a file and not a directory: " + targetDir); + } + File targetParentDir = targetDir.getParentFile(); + if (targetParentDir == null) + { + throw new AlfrescoRuntimeException("Target location may not be a root directory: " + targetDir); + } + File tempDir = new File(targetParentDir, "indexbackup_temp"); + + // get all the available stores + List storeRefs = nodeService.getStores(); + + // lock all the stores + List lockedStores = new ArrayList(storeRefs.size()); + try + { + for (StoreRef storeRef : storeRefs) + { + factory.luceneIndexLock.getWriteLock(storeRef); + lockedStores.add(storeRef); + } + File indexRootDir = new File(factory.indexRootLocation); + // perform the copy + backupDirectory(indexRootDir, tempDir, targetDir); + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException("Failed to copy Lucene index root: \n" + + " Index root: " + factory.indexRootLocation + "\n" + + " Target: " + targetDir, + e); + } + finally + { + for (StoreRef storeRef : lockedStores) + { + try + { + factory.luceneIndexLock.releaseWriteLock(storeRef); + } + catch (Throwable e) + { + logger.error("Failed to release index lock for store " + storeRef, e); + } + } + } + if (logger.isDebugEnabled()) + { + logger.debug("Backed up Lucene indexes: \n" + + " Target directory: " + targetDir); + } + } + + /** + * Makes a backup of the source directory via a temporary folder + * @param storeRef + */ + private void backupDirectory(File sourceDir, File tempDir, File targetDir) throws Exception + { + if (!sourceDir.exists()) + { + // there is nothing to copy + return; + } + // delete the files from the temp directory + if (tempDir.exists()) + { + FileUtils.deleteDirectory(tempDir); + if (tempDir.exists()) + { + throw new AlfrescoRuntimeException("Temp directory exists and cannot be deleted: " + tempDir); + } + } + // copy to the temp directory + FileUtils.copyDirectory(sourceDir, tempDir, true); + // check that the temp directory was created + if (!tempDir.exists()) + { + throw new AlfrescoRuntimeException("Copy to temp location failed"); + } + // delete the target directory + FileUtils.deleteDirectory(targetDir); + if (targetDir.exists()) + { + throw new AlfrescoRuntimeException("Failed to delete older files from target location"); + } + // rename the temp to be the target + tempDir.renameTo(targetDir); + // make sure the rename worked + if (!targetDir.exists()) + { + throw new AlfrescoRuntimeException("Failed to rename temporary directory to target backup directory"); + } + } + } + + /** + * Job that lock uses the {@link LuceneIndexBackupComponent} to perform safe backups of the Lucene indexes. + * + * @author Derek Hulley + */ + public static class LuceneIndexBackupJob implements Job + { + /** KEY_LUCENE_INDEX_BACKUP_COMPONENT = 'luceneIndexBackupComponent' */ + public static final String KEY_LUCENE_INDEX_BACKUP_COMPONENT = "luceneIndexBackupComponent"; + + /** + * Locks the Lucene indexes and copies them to a backup location + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap jobData = context.getJobDetail().getJobDataMap(); + LuceneIndexBackupComponent backupComponent = (LuceneIndexBackupComponent) jobData.get(KEY_LUCENE_INDEX_BACKUP_COMPONENT); + if (backupComponent == null) + { + throw new JobExecutionException("Missing job data: " + KEY_LUCENE_INDEX_BACKUP_COMPONENT); + } + // perform the backup + backupComponent.backup(); + } + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerImpl.java new file mode 100644 index 0000000000..e0110da164 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneIndexerImpl.java @@ -0,0 +1,1841 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import javax.transaction.Status; +import javax.transaction.xa.XAResource; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.search.ISO9075; +import org.alfresco.repo.search.IndexerException; +import org.alfresco.repo.search.impl.lucene.fts.FTSIndexerAware; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NoTransformerException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; +import org.apache.log4j.Logger; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Hits; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.TermQuery; + +/** + * The implementation of the lucene based indexer. Supports basic transactional + * behaviour if used on its own. + * + * @author andyh + * + */ +public class LuceneIndexerImpl extends LuceneBase implements LuceneIndexer +{ + public static final String NOT_INDEXED_NO_TRANSFORMATION = "nint"; + + public static final String NOT_INDEXED_TRANSFORMATION_FAILED = "nitf"; + + public static final String NOT_INDEXED_CONTENT_MISSING = "nicm"; + + private static Logger s_logger = Logger.getLogger(LuceneIndexerImpl.class); + + /** + * Enum for indexing actions against a node + */ + private enum Action + { + INDEX, REINDEX, DELETE, CASCADEREINDEX + }; + + /** + * The node service we use to get information about nodes + */ + private NodeService nodeService; + + /** + * Content service to get content for indexing. + */ + private ContentService contentService; + + /** + * A list of all deletions we have made - at merge these deletions need to + * be made against the main index. + * + * TODO: Consider if this information needs to be persisted for recovery + */ + + private Set deletions = new LinkedHashSet(); + + /** + * The status of this index - follows javax.transaction.Status + */ + + private int status = Status.STATUS_UNKNOWN; + + /** + * Has this index been modified? + */ + + private boolean isModified = false; + + /** + * Flag to indicte if we are doing an in transactional delta or a batch + * update to the index. If true, we are just fixing up non atomically + * indexed things from one or more other updates. + */ + + private Boolean isFTSUpdate = null; + + /** + * List of pending indexing commands. + */ + private List commandList = new ArrayList(10000); + + /** + * Call back to make after doing non atomic indexing + */ + private FTSIndexerAware callBack; + + /** + * Count of remaining items to index non atomically + */ + private int remainingCount = 0; + + /** + * A list of stuff that requires non atomic indexing + */ + private ArrayList toFTSIndex = new ArrayList(); + + /** + * Default construction + * + */ + LuceneIndexerImpl() + { + super(); + } + + /** + * IOC setting of dictionary service + */ + + public void setDictionaryService(DictionaryService dictionaryService) + { + super.setDictionaryService(dictionaryService); + } + + /** + * Setter for getting the node service via IOC Used in the Spring container + * + * @param nodeService + */ + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * IOC setting of the content service + * + * @param contentService + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /*************************************************************************** + * * Indexer Implementation * ************************** + */ + + /** + * Utility method to check we are in the correct state to do work Also keeps + * track of the dirty flag. + * + */ + + private void checkAbleToDoWork(boolean isFTS, boolean isModified) + { + if (isFTSUpdate == null) + { + isFTSUpdate = Boolean.valueOf(isFTS); + } + else + { + if (isFTS != isFTSUpdate.booleanValue()) + { + throw new IndexerException("Can not mix FTS and transactional updates"); + } + } + + switch (status) + { + case Status.STATUS_UNKNOWN: + status = Status.STATUS_ACTIVE; + break; + case Status.STATUS_ACTIVE: + // OK + break; + default: + // All other states are a problem + throw new IndexerException(buildErrorString()); + } + this.isModified = isModified; + } + + /** + * Utility method to report errors about invalid state. + * + * @return + */ + private String buildErrorString() + { + StringBuilder buffer = new StringBuilder(128); + buffer.append("The indexer is unable to accept more work: "); + switch (status) + { + case Status.STATUS_COMMITTED: + buffer.append("The indexer has been committed"); + break; + case Status.STATUS_COMMITTING: + buffer.append("The indexer is committing"); + break; + case Status.STATUS_MARKED_ROLLBACK: + buffer.append("The indexer is marked for rollback"); + break; + case Status.STATUS_PREPARED: + buffer.append("The indexer is prepared to commit"); + break; + case Status.STATUS_PREPARING: + buffer.append("The indexer is preparing to commit"); + break; + case Status.STATUS_ROLLEDBACK: + buffer.append("The indexer has been rolled back"); + break; + case Status.STATUS_ROLLING_BACK: + buffer.append("The indexer is rolling back"); + break; + case Status.STATUS_UNKNOWN: + buffer.append("The indexer is in an unknown state"); + break; + default: + break; + } + return buffer.toString(); + } + + /* + * Indexer Implementation + */ + + public void createNode(ChildAssociationRef relationshipRef) throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Create node " + relationshipRef.getChildRef()); + } + checkAbleToDoWork(false, true); + try + { + NodeRef childRef = relationshipRef.getChildRef(); + // If we have the root node we delete all other root nodes first + if ((relationshipRef.getParentRef() == null) + && childRef.equals(nodeService.getRootNode(childRef.getStoreRef()))) + { + addRootNodesToDeletionList(); + s_logger.warn("Detected root node addition: deleting all nodes from the index"); + } + index(childRef); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Create node failed", e); + } + } + + private void addRootNodesToDeletionList() + { + IndexReader mainReader = null; + try + { + try + { + mainReader = getReader(); + TermDocs td = mainReader.termDocs(new Term("ISROOT", "T")); + while (td.next()) + { + int doc = td.doc(); + Document document = mainReader.document(doc); + String id = document.get("ID"); + NodeRef ref = new NodeRef(id); + deleteImpl(ref, false, true, mainReader); + } + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to delete all primary nodes", e); + } + } + finally + { + if (mainReader != null) + { + try + { + mainReader.close(); + } + catch (IOException e) + { + throw new LuceneIndexException("Filed to close main reader", e); + } + } + } + } + + public void updateNode(NodeRef nodeRef) throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Update node " + nodeRef); + } + checkAbleToDoWork(false, true); + try + { + reindex(nodeRef, false); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Update node failed", e); + } + } + + public void deleteNode(ChildAssociationRef relationshipRef) throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Delete node " + relationshipRef.getChildRef()); + } + checkAbleToDoWork(false, true); + try + { + delete(relationshipRef.getChildRef()); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Delete node failed", e); + } + } + + public void createChildRelationship(ChildAssociationRef relationshipRef) throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Create child " + relationshipRef); + } + checkAbleToDoWork(false, true); + try + { + // TODO: Optimise + // reindex(relationshipRef.getParentRef()); + reindex(relationshipRef.getChildRef(), true); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Failed to create child relationship", e); + } + } + + public void updateChildRelationship(ChildAssociationRef relationshipBeforeRef, + ChildAssociationRef relationshipAfterRef) throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Update child " + relationshipBeforeRef + " to " + relationshipAfterRef); + } + checkAbleToDoWork(false, true); + try + { + // TODO: Optimise + if (relationshipBeforeRef.getParentRef() != null) + { + // reindex(relationshipBeforeRef.getParentRef()); + } + reindex(relationshipBeforeRef.getChildRef(), true); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Failed to update child relationship", e); + } + } + + public void deleteChildRelationship(ChildAssociationRef relationshipRef) throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Delete child " + relationshipRef); + } + checkAbleToDoWork(false, true); + try + { + // TODO: Optimise + if (relationshipRef.getParentRef() != null) + { + // reindex(relationshipRef.getParentRef()); + } + reindex(relationshipRef.getChildRef(), true); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Failed to delete child relationship", e); + } + } + + /** + * Generate an indexer + * + * @param storeRef + * @param deltaId + * @return + */ + public static LuceneIndexerImpl getUpdateIndexer(StoreRef storeRef, String deltaId, LuceneConfig config) + throws LuceneIndexException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Creating indexer"); + } + LuceneIndexerImpl indexer = new LuceneIndexerImpl(); + indexer.setLuceneConfig(config); + indexer.initialise(storeRef, deltaId, false, true); + return indexer; + } + + /* + * Transactional support Used by the resource manager for indexers. + */ + + /** + * Commit this index + */ + + public void commit() throws LuceneIndexException + { + switch (status) + { + case Status.STATUS_COMMITTING: + throw new LuceneIndexException("Unable to commit: Transaction is committing"); + case Status.STATUS_COMMITTED: + throw new LuceneIndexException("Unable to commit: Transaction is commited "); + case Status.STATUS_ROLLING_BACK: + throw new LuceneIndexException("Unable to commit: Transaction is rolling back"); + case Status.STATUS_ROLLEDBACK: + throw new LuceneIndexException("Unable to commit: Transaction is aleady rolled back"); + case Status.STATUS_MARKED_ROLLBACK: + throw new LuceneIndexException("Unable to commit: Transaction is marked for roll back"); + case Status.STATUS_PREPARING: + throw new LuceneIndexException("Unable to commit: Transaction is preparing"); + case Status.STATUS_ACTIVE: + // special case - commit from active + prepare(); + // drop through to do the commit; + default: + if (status != Status.STATUS_PREPARED) + { + throw new LuceneIndexException("Index must be prepared to commit"); + } + status = Status.STATUS_COMMITTING; + try + { + if (isModified()) + { + if (isFTSUpdate.booleanValue()) + { + doFTSIndexCommit(); + // FTS does not trigger indexing request + } + else + { + // Build the deletion terms + Set terms = new LinkedHashSet(); + for (NodeRef nodeRef : deletions) + { + terms.add(new Term("ID", nodeRef.toString())); + } + // Merge + mergeDeltaIntoMain(terms); + luceneFullTextSearchIndexer.requiresIndex(store); + } + } + status = Status.STATUS_COMMITTED; + if (callBack != null) + { + callBack.indexCompleted(store, remainingCount, null); + } + } + catch (LuceneIndexException e) + { + // If anything goes wrong we try and do a roll back + rollback(); + throw new LuceneIndexException("Commit failed", e); + } + finally + { + // Make sure we tidy up + deleteDelta(); + } + break; + } + } + + private void doFTSIndexCommit() throws LuceneIndexException + { + IndexReader mainReader = null; + IndexReader deltaReader = null; + IndexSearcher mainSearcher = null; + IndexSearcher deltaSearcher = null; + + try + { + mainReader = getReader(); + deltaReader = getDeltaReader(); + mainSearcher = new IndexSearcher(mainReader); + deltaSearcher = new IndexSearcher(deltaReader); + + for (Helper helper : toFTSIndex) + { + BooleanQuery query = new BooleanQuery(); + query.add(new TermQuery(new Term("ID", helper.nodeRef.toString())), true, false); + query.add(new TermQuery(new Term("TX", helper.tx)), true, false); + query.add(new TermQuery(new Term("ISNODE", "T")), false, false); + + try + { + Hits hits = mainSearcher.search(query); + if (hits.length() > 0) + { + // No change + for (int i = 0; i < hits.length(); i++) + { + mainReader.delete(hits.id(i)); + } + } + else + { + hits = deltaSearcher.search(query); + for (int i = 0; i < hits.length(); i++) + { + deltaReader.delete(hits.id(i)); + } + } + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to delete an FTS update from the original index", e); + } + } + + } + finally + { + if (deltaSearcher != null) + { + try + { + deltaSearcher.close(); + } + catch (IOException e) + { + s_logger.warn("Failed to close delta searcher", e); + } + } + if (mainSearcher != null) + { + try + { + mainSearcher.close(); + } + catch (IOException e) + { + s_logger.warn("Failed to close main searcher", e); + } + } + try + { + closeDeltaReader(); + } + catch (LuceneIndexException e) + { + s_logger.warn("Failed to close delta reader", e); + } + if (mainReader != null) + { + try + { + mainReader.close(); + } + catch (IOException e) + { + s_logger.warn("Failed to close main reader", e); + } + } + } + + mergeDeltaIntoMain(new LinkedHashSet()); + + } + + /** + * Prepare to commit + * + * At the moment this makes sure we have all the locks + * + * TODO: This is not doing proper serialisation against the index as would a + * data base transaction. + * + * @return + */ + public int prepare() throws LuceneIndexException + { + + switch (status) + { + case Status.STATUS_COMMITTING: + throw new IndexerException("Unable to prepare: Transaction is committing"); + case Status.STATUS_COMMITTED: + throw new IndexerException("Unable to prepare: Transaction is commited "); + case Status.STATUS_ROLLING_BACK: + throw new IndexerException("Unable to prepare: Transaction is rolling back"); + case Status.STATUS_ROLLEDBACK: + throw new IndexerException("Unable to prepare: Transaction is aleady rolled back"); + case Status.STATUS_MARKED_ROLLBACK: + throw new IndexerException("Unable to prepare: Transaction is marked for roll back"); + case Status.STATUS_PREPARING: + throw new IndexerException("Unable to prepare: Transaction is already preparing"); + case Status.STATUS_PREPARED: + throw new IndexerException("Unable to prepare: Transaction is already prepared"); + default: + status = Status.STATUS_PREPARING; + try + { + if (isModified()) + { + saveDelta(); + flushPending(); + prepareToMergeIntoMain(); + } + status = Status.STATUS_PREPARED; + return isModified ? XAResource.XA_OK : XAResource.XA_RDONLY; + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Index failed to prepare", e); + } + } + } + + /** + * Has this index been modified? + * + * @return + */ + public boolean isModified() + { + return isModified; + } + + /** + * Return the javax.transaction.Status integer status code + * + * @return + */ + public int getStatus() + { + return status; + } + + /** + * Roll back the index changes (this just means they are never added) + * + */ + + public void rollback() throws LuceneIndexException + { + switch (status) + { + + case Status.STATUS_COMMITTED: + throw new IndexerException("Unable to roll back: Transaction is committed "); + case Status.STATUS_ROLLING_BACK: + throw new IndexerException("Unable to roll back: Transaction is rolling back"); + case Status.STATUS_ROLLEDBACK: + throw new IndexerException("Unable to roll back: Transaction is already rolled back"); + case Status.STATUS_COMMITTING: + // Can roll back during commit + default: + status = Status.STATUS_ROLLING_BACK; + if (isModified()) + { + deleteDelta(); + } + status = Status.STATUS_ROLLEDBACK; + if (callBack != null) + { + callBack.indexCompleted(store, 0, null); + } + break; + } + } + + /** + * Mark this index for roll back only. This action can not be reversed. It + * will reject all other work and only allow roll back. + * + */ + + public void setRollbackOnly() + { + switch (status) + { + case Status.STATUS_COMMITTING: + throw new IndexerException("Unable to mark for rollback: Transaction is committing"); + case Status.STATUS_COMMITTED: + throw new IndexerException("Unable to mark for rollback: Transaction is committed"); + default: + status = Status.STATUS_MARKED_ROLLBACK; + break; + } + } + + /* + * Implementation + */ + + private void index(NodeRef nodeRef) throws LuceneIndexException + { + addCommand(new Command(nodeRef, Action.INDEX)); + } + + private void reindex(NodeRef nodeRef, boolean cascadeReindexDirectories) throws LuceneIndexException + { + addCommand(new Command(nodeRef, cascadeReindexDirectories ? Action.CASCADEREINDEX : Action.REINDEX)); + } + + private void delete(NodeRef nodeRef) throws LuceneIndexException + { + addCommand(new Command(nodeRef, Action.DELETE)); + } + + private void addCommand(Command command) + { + if (commandList.size() > 0) + { + Command last = commandList.get(commandList.size() - 1); + if ((last.action == command.action) && (last.nodeRef.equals(command.nodeRef))) + { + return; + } + } + purgeCommandList(command); + commandList.add(command); + + if (commandList.size() > getLuceneConfig().getIndexerBatchSize()) + { + flushPending(); + } + } + + private void purgeCommandList(Command command) + { + if (command.action == Action.DELETE) + { + removeFromCommandList(command, false); + } + else if (command.action == Action.REINDEX) + { + removeFromCommandList(command, true); + } + else if (command.action == Action.INDEX) + { + removeFromCommandList(command, true); + } + else if (command.action == Action.CASCADEREINDEX) + { + removeFromCommandList(command, true); + } + } + + private void removeFromCommandList(Command command, boolean matchExact) + { + for (ListIterator it = commandList.listIterator(commandList.size()); it.hasPrevious(); /**/) + { + Command current = it.previous(); + if (matchExact) + { + if ((current.action == command.action) && (current.nodeRef.equals(command.nodeRef))) + { + it.remove(); + return; + } + } + else + { + if (current.nodeRef.equals(command.nodeRef)) + { + it.remove(); + } + } + } + } + + public void flushPending() throws LuceneIndexException + { + IndexReader mainReader = null; + try + { + mainReader = getReader(); + Set forIndex = new LinkedHashSet(); + + for (Command command : commandList) + { + if (command.action == Action.INDEX) + { + // Indexing just requires the node to be added to the list + forIndex.add(command.nodeRef); + } + else if (command.action == Action.REINDEX) + { + // Reindex is a delete and then and index + Set set = deleteImpl(command.nodeRef, true, false, mainReader); + + // Deleting any pending index actions + // - make sure we only do at most one index + forIndex.removeAll(set); + // Add the nodes for index + forIndex.addAll(set); + } + else if (command.action == Action.CASCADEREINDEX) + { + // Reindex is a delete and then and index + Set set = deleteImpl(command.nodeRef, true, true, mainReader); + + // Deleting any pending index actions + // - make sure we only do at most one index + forIndex.removeAll(set); + // Add the nodes for index + forIndex.addAll(set); + } + else if (command.action == Action.DELETE) + { + // Delete the nodes + Set set = deleteImpl(command.nodeRef, false, true, mainReader); + // Remove any pending indexes + forIndex.removeAll(set); + } + } + commandList.clear(); + indexImpl(forIndex, false); + } + finally + { + if (mainReader != null) + { + try + { + mainReader.close(); + } + catch (IOException e) + { + throw new LuceneIndexException("Filed to close main reader", e); + } + } + closeDeltaWriter(); + } + } + + private Set deleteImpl(NodeRef nodeRef, boolean forReindex, boolean cascade, IndexReader mainReader) + throws LuceneIndexException + { + // startTimer(); + getDeltaReader(); + // outputTime("Delete "+nodeRef+" size = "+getDeltaWriter().docCount()); + Set refs = new LinkedHashSet(); + + refs.addAll(deleteContainerAndBelow(nodeRef, getDeltaReader(), true, cascade)); + refs.addAll(deleteContainerAndBelow(nodeRef, mainReader, false, cascade)); + + if (!forReindex) + { + Set leafrefs = new LinkedHashSet(); + + leafrefs.addAll(deletePrimary(refs, getDeltaReader(), true)); + leafrefs.addAll(deletePrimary(refs, mainReader, false)); + + leafrefs.addAll(deleteReference(refs, getDeltaReader(), true)); + leafrefs.addAll(deleteReference(refs, mainReader, false)); + + refs.addAll(leafrefs); + } + + deletions.addAll(refs); + + return refs; + + } + + private Set deletePrimary(Collection nodeRefs, IndexReader reader, boolean delete) + throws LuceneIndexException + { + + Set refs = new LinkedHashSet(); + + for (NodeRef nodeRef : nodeRefs) + { + + try + { + TermDocs td = reader.termDocs(new Term("PRIMARYPARENT", nodeRef.toString())); + while (td.next()) + { + int doc = td.doc(); + Document document = reader.document(doc); + String id = document.get("ID"); + NodeRef ref = new NodeRef(id); + refs.add(ref); + if (delete) + { + reader.delete(doc); + } + } + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to delete node by primary parent for " + nodeRef.toString(), e); + } + } + + return refs; + + } + + private Set deleteReference(Collection nodeRefs, IndexReader reader, boolean delete) + throws LuceneIndexException + { + + Set refs = new LinkedHashSet(); + + for (NodeRef nodeRef : nodeRefs) + { + + try + { + TermDocs td = reader.termDocs(new Term("PARENT", nodeRef.toString())); + while (td.next()) + { + int doc = td.doc(); + Document document = reader.document(doc); + String id = document.get("ID"); + NodeRef ref = new NodeRef(id); + refs.add(ref); + if (delete) + { + reader.delete(doc); + } + } + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to delete node by parent for " + nodeRef.toString(), e); + } + } + + return refs; + + } + + private Set deleteContainerAndBelow(NodeRef nodeRef, IndexReader reader, boolean delete, boolean cascade) + throws LuceneIndexException + { + Set refs = new LinkedHashSet(); + + try + { + if (delete) + { + reader.delete(new Term("ID", nodeRef.toString())); + } + refs.add(nodeRef); + if (cascade) + { + TermDocs td = reader.termDocs(new Term("ANCESTOR", nodeRef.toString())); + while (td.next()) + { + int doc = td.doc(); + Document document = reader.document(doc); + String id = document.get("ID"); + NodeRef ref = new NodeRef(id); + refs.add(ref); + if (delete) + { + reader.delete(doc); + } + } + } + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to delete container and below for " + nodeRef.toString(), e); + } + return refs; + } + + private void indexImpl(Set nodeRefs, boolean isNew) throws LuceneIndexException + { + for (NodeRef ref : nodeRefs) + { + indexImpl(ref, isNew); + } + } + + private void indexImpl(NodeRef nodeRef, boolean isNew) throws LuceneIndexException + { + IndexWriter writer = getDeltaWriter(); + + // avoid attempting to index nodes that don't exist + + try + { + List docs = createDocuments(nodeRef, isNew, false, true); + for (Document doc : docs) + { + try + { + writer.addDocument(doc /* + * TODO: Select the language based + * analyser + */); + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to add document to index", e); + } + } + } + catch (InvalidNodeRefException e) + { + // The node does not exist + return; + } + + } + + static class Counter + { + int countInParent = 0; + + int count = -1; + + int getCountInParent() + { + return countInParent; + } + + int getRepeat() + { + return (count / countInParent) + 1; + } + + void incrementParentCount() + { + countInParent++; + } + + void increment() + { + count++; + } + + } + + private class Pair + { + private F first; + + private S second; + + public Pair(F first, S second) + { + this.first = first; + this.second = second; + } + + public F getFirst() + { + return first; + } + + public S getSecond() + { + return second; + } + } + + private List createDocuments(NodeRef nodeRef, boolean isNew, boolean indexAllProperties, + boolean includeDirectoryDocuments) + { + Map nodeCounts = getNodeCounts(nodeRef); + List docs = new ArrayList(); + ChildAssociationRef qNameRef = null; + Map properties = nodeService.getProperties(nodeRef); + NodeRef.Status nodeStatus = nodeService.getNodeStatus(nodeRef); + + Collection directPaths = nodeService.getPaths(nodeRef, false); + Collection> categoryPaths = getCategoryPaths(nodeRef, properties); + Collection> paths = new ArrayList>(directPaths.size() + + categoryPaths.size()); + for (Path path : directPaths) + { + paths.add(new Pair(path, null)); + } + paths.addAll(categoryPaths); + + Document xdoc = new Document(); + xdoc.add(new Field("ID", nodeRef.toString(), true, true, false)); + xdoc.add(new Field("TX", nodeStatus.getChangeTxnId(), true, true, false)); + boolean isAtomic = true; + for (QName propertyName : properties.keySet()) + { + Serializable value = properties.get(propertyName); + isAtomic = indexProperty(nodeRef, propertyName, value, xdoc, isAtomic, true); + if (indexAllProperties) + { + indexProperty(nodeRef, propertyName, value, xdoc, false, false); + } + } + + boolean isRoot = nodeRef.equals(nodeService.getRootNode(nodeRef.getStoreRef())); + + StringBuilder parentBuffer = new StringBuilder(); + StringBuilder qNameBuffer = new StringBuilder(64); + + int containerCount = 0; + for (Iterator> it = paths.iterator(); it.hasNext(); /**/) + { + Pair pair = it.next(); + // Lucene flags in order are: Stored, indexed, tokenised + + qNameRef = getLastRefOrNull(pair.getFirst()); + + String pathString = pair.getFirst().toString(); + if ((pathString.length() > 0) && (pathString.charAt(0) == '/')) + { + pathString = pathString.substring(1); + } + + if (isRoot) + { + // Root node + } + else if (pair.getFirst().size() == 1) + { + // Pseudo root node ignore + } + else + // not a root node + { + Counter counter = nodeCounts.get(qNameRef); + // If we have something in a container with root aspect we will + // not find it + + if ((counter == null) || (counter.getRepeat() < counter.getCountInParent())) + { + if ((qNameRef != null) && (qNameRef.getParentRef() != null) && (qNameRef.getQName() != null)) + { + if (qNameBuffer.length() > 0) + { + qNameBuffer.append(";/"); + } + qNameBuffer.append(ISO9075.getXPathName(qNameRef.getQName())); + xdoc.add(new Field("PARENT", qNameRef.getParentRef().toString(), true, true, false)); + xdoc.add(new Field("ASSOCTYPEQNAME", ISO9075.getXPathName(qNameRef.getTypeQName()), true, + false, false)); + xdoc.add(new Field("LINKASPECT", (pair.getSecond() == null) ? "" : ISO9075.getXPathName(pair + .getSecond()), true, true, false)); + } + } + + if (counter != null) + { + counter.increment(); + } + + // TODO: DC: Should this also include aspect child definitions? + QName nodeTypeRef = nodeService.getType(nodeRef); + TypeDefinition nodeTypeDef = getDictionaryService().getType(nodeTypeRef); + // check for child associations + + if (includeDirectoryDocuments) + { + if (nodeTypeDef.getChildAssociations().size() > 0) + { + if (directPaths.contains(pair.getFirst())) + { + Document directoryEntry = new Document(); + directoryEntry.add(new Field("ID", nodeRef.toString(), true, true, false)); + directoryEntry.add(new Field("PATH", pathString, true, true, true)); + for (NodeRef parent : getParents(pair.getFirst())) + { + directoryEntry.add(new Field("ANCESTOR", parent.toString(), false, true, false)); + } + directoryEntry.add(new Field("ISCONTAINER", "T", true, true, false)); + + if (isCategory(getDictionaryService().getType(nodeService.getType(nodeRef)))) + { + directoryEntry.add(new Field("ISCATEGORY", "T", true, true, false)); + } + + docs.add(directoryEntry); + } + } + } + } + } + + // Root Node + if (isRoot) + { + // TODO: Does the root element have a QName? + xdoc.add(new Field("ISCONTAINER", "T", true, true, false)); + xdoc.add(new Field("PATH", "", true, true, true)); + xdoc.add(new Field("QNAME", "", true, true, true)); + xdoc.add(new Field("ISROOT", "T", false, true, false)); + xdoc.add(new Field("PRIMARYASSOCTYPEQNAME", ISO9075.getXPathName(ContentModel.ASSOC_CHILDREN), true, false, + false)); + xdoc.add(new Field("ISNODE", "T", false, true, false)); + docs.add(xdoc); + + } + else + // not a root node + { + xdoc.add(new Field("QNAME", qNameBuffer.toString(), true, true, true)); + // xdoc.add(new Field("PARENT", parentBuffer.toString(), true, true, + // true)); + + ChildAssociationRef primary = nodeService.getPrimaryParent(nodeRef); + xdoc.add(new Field("PRIMARYPARENT", primary.getParentRef().toString(), true, true, false)); + xdoc.add(new Field("PRIMARYASSOCTYPEQNAME", ISO9075.getXPathName(primary.getTypeQName()), true, false, + false)); + QName typeQName = nodeService.getType(nodeRef); + + xdoc.add(new Field("TYPE", ISO9075.getXPathName(typeQName), true, true, false)); + for (QName classRef : nodeService.getAspects(nodeRef)) + { + xdoc.add(new Field("ASPECT", ISO9075.getXPathName(classRef), true, true, false)); + } + + xdoc.add(new Field("ISROOT", "F", false, true, false)); + xdoc.add(new Field("ISNODE", "T", false, true, false)); + if (isAtomic || indexAllProperties) + { + xdoc.add(new Field("FTSSTATUS", "Clean", false, true, false)); + } + else + { + if (isNew) + { + xdoc.add(new Field("FTSSTATUS", "New", false, true, false)); + } + else + { + xdoc.add(new Field("FTSSTATUS", "Dirty", false, true, false)); + } + } + + // { + docs.add(xdoc); + // } + } + + return docs; + } + + private ArrayList getParents(Path path) + { + ArrayList parentsInDepthOrderStartingWithSelf = new ArrayList(8); + for (Iterator elit = path.iterator(); elit.hasNext(); /**/) + { + Path.Element element = elit.next(); + if (!(element instanceof Path.ChildAssocElement)) + { + throw new IndexerException("Confused path: " + path); + } + Path.ChildAssocElement cae = (Path.ChildAssocElement) element; + parentsInDepthOrderStartingWithSelf.add(0, cae.getRef().getChildRef()); + + } + return parentsInDepthOrderStartingWithSelf; + } + + private ChildAssociationRef getLastRefOrNull(Path path) + { + if (path.last() instanceof Path.ChildAssocElement) + { + Path.ChildAssocElement cae = (Path.ChildAssocElement) path.last(); + return cae.getRef(); + } + else + { + return null; + } + } + + private boolean indexProperty(NodeRef nodeRef, QName propertyName, Serializable value, Document doc, + boolean isAtomic, boolean indexAtomicProperties) + { + String attributeName = "@" + + QName.createQName(propertyName.getNamespaceURI(), ISO9075.encode(propertyName.getLocalName())); + + boolean store = true; + boolean index = true; + boolean tokenise = true; + boolean atomic = true; + boolean isContent = false; + + PropertyDefinition propertyDef = getDictionaryService().getProperty(propertyName); + if (propertyDef != null) + { + index = propertyDef.isIndexed(); + store = propertyDef.isStoredInIndex(); + tokenise = propertyDef.isTokenisedInIndex(); + atomic = propertyDef.isIndexedAtomically(); + isContent = propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT); + } + isAtomic &= atomic; + + if (value != null) + { + if (indexAtomicProperties == atomic) + { + if (!indexAtomicProperties) + { + doc.removeFields(propertyName.toString()); + } + // convert value to String + for (String strValue : DefaultTypeConverter.INSTANCE.getCollection(String.class, value)) + { + if (strValue != null) + { + // String strValue = ValueConverter.convert(String.class, value); + // TODO: Need to add with the correct language based analyser + + if (isContent) + { + ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, value); + if (index) + { + ContentReader reader = contentService.getReader(nodeRef, propertyName); + if (reader != null && reader.exists()) + { + boolean readerReady = true; + // transform if necessary (it is not a UTF-8 + // text document) + if (!EqualsHelper.nullSafeEquals(reader.getMimetype(), + MimetypeMap.MIMETYPE_TEXT_PLAIN) + || !EqualsHelper.nullSafeEquals(reader.getEncoding(), "UTF-8")) + { + ContentWriter writer = contentService.getTempWriter(); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + // this is what the analyzers expect on the stream + writer.setEncoding("UTF-8"); + try + { + contentService.transform(reader, writer); + // point the reader to the new-written content + reader = writer.getReader(); + } + catch (NoTransformerException e) + { + // log it + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Not indexed: No transformation", e); + } + // don't index from the reader + readerReady = false; + // not indexed: no transformation + doc.add(Field.Text("TEXT", NOT_INDEXED_NO_TRANSFORMATION)); + doc.add(Field.Text(attributeName, NOT_INDEXED_NO_TRANSFORMATION)); + } + catch (ContentIOException e) + { + // log it + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Not indexed: Transformation failed", e); + } + // don't index from the reader + readerReady = false; + // not indexed: transformation + // failed + doc.add(Field.Text("TEXT", NOT_INDEXED_TRANSFORMATION_FAILED)); + doc.add(Field.Text(attributeName, NOT_INDEXED_TRANSFORMATION_FAILED)); + } + } + // add the text field using the stream from the + // reader, but only if the reader is valid + if (readerReady) + { + InputStreamReader isr = null; + InputStream ris = reader.getContentInputStream(); + try + { + isr = new InputStreamReader(ris,"UTF-8"); + } + catch (UnsupportedEncodingException e) + { + isr = new InputStreamReader(ris); + } + doc.add(Field.Text("TEXT", isr)); + + + ris = reader.getReader().getContentInputStream(); + try + { + isr = new InputStreamReader(ris,"UTF-8"); + } + catch (UnsupportedEncodingException e) + { + isr = new InputStreamReader(ris); + } + + doc.add(Field.Text("@" + + QName.createQName(propertyName.getNamespaceURI(), ISO9075 + .encode(propertyName.getLocalName())), isr)); + + + doc.add(new Field(attributeName+".mimetype", contentData.getMimetype(), false, true, false)); + } + } + + else + // URL not present (null reader) or no content at the URL (file missing) + { + // log it + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Not indexed: Content Missing \n" + + " node: " + nodeRef + "\n" + " reader: " + reader + "\n" + + " content exists: " + + (reader == null ? " --- " : Boolean.toString(reader.exists()))); + } + // not indexed: content missing + doc.add(Field.Text("TEXT", NOT_INDEXED_CONTENT_MISSING)); + doc.add(Field.Text(attributeName, NOT_INDEXED_CONTENT_MISSING)); + } + } + } + else + { + doc.add(new Field(attributeName, strValue, store, index, tokenise)); + } + } + } + } + } + + return isAtomic; + } + + private Map getNodeCounts(NodeRef nodeRef) + { + Map nodeCounts = new HashMap(5); + List parentAssocs = nodeService.getParentAssocs(nodeRef); + // count the number of times the association is duplicated + for (ChildAssociationRef assoc : parentAssocs) + { + Counter counter = nodeCounts.get(assoc); + if (counter == null) + { + counter = new Counter(); + nodeCounts.put(assoc, counter); + } + counter.incrementParentCount(); + + } + return nodeCounts; + } + + private Collection> getCategoryPaths(NodeRef nodeRef, Map properties) + { + ArrayList> categoryPaths = new ArrayList>(); + Set aspects = nodeService.getAspects(nodeRef); + + for (QName classRef : aspects) + { + AspectDefinition aspDef = getDictionaryService().getAspect(classRef); + if (isCategorised(aspDef)) + { + LinkedList> aspectPaths = new LinkedList>(); + for (PropertyDefinition propDef : aspDef.getProperties().values()) + { + if (propDef.getDataType().getName().equals(DataTypeDefinition.CATEGORY)) + { + for (NodeRef catRef : DefaultTypeConverter.INSTANCE.getCollection(NodeRef.class, properties + .get(propDef.getName()))) + { + if (catRef != null) + { + for (Path path : nodeService.getPaths(catRef, false)) + { + if ((path.size() > 1) && (path.get(1) instanceof Path.ChildAssocElement)) + { + Path.ChildAssocElement cae = (Path.ChildAssocElement) path.get(1); + boolean isFakeRoot = true; + for (ChildAssociationRef car : nodeService.getParentAssocs(cae.getRef() + .getChildRef())) + { + if (cae.getRef().equals(car)) + { + isFakeRoot = false; + break; + } + } + if (isFakeRoot) + { + if (path.toString().indexOf(aspDef.getName().toString()) != -1) + { + aspectPaths.add(new Pair(path, aspDef.getName())); + } + } + } + } + + } + } + } + } + categoryPaths.addAll(aspectPaths); + } + } + // Add member final element + for (Pair pair : categoryPaths) + { + if (pair.getFirst().last() instanceof Path.ChildAssocElement) + { + Path.ChildAssocElement cae = (Path.ChildAssocElement) pair.getFirst().last(); + ChildAssociationRef assocRef = cae.getRef(); + pair.getFirst().append( + new Path.ChildAssocElement(new ChildAssociationRef(assocRef.getTypeQName(), assocRef + .getChildRef(), QName.createQName("member"), nodeRef))); + } + } + + return categoryPaths; + } + + private boolean isCategorised(AspectDefinition aspDef) + { + AspectDefinition current = aspDef; + while (current != null) + { + if (current.getName().equals(ContentModel.ASPECT_CLASSIFIABLE)) + { + return true; + } + else + { + QName parentName = current.getParentName(); + if (parentName == null) + { + break; + } + current = getDictionaryService().getAspect(parentName); + } + } + return false; + } + + private boolean isCategory(TypeDefinition typeDef) + { + if (typeDef == null) + { + return false; + } + TypeDefinition current = typeDef; + while (current != null) + { + if (current.getName().equals(ContentModel.TYPE_CATEGORY)) + { + return true; + } + else + { + QName parentName = current.getParentName(); + if (parentName == null) + { + break; + } + current = getDictionaryService().getType(parentName); + } + } + return false; + } + + public void updateFullTextSearch(int size) throws LuceneIndexException + { + checkAbleToDoWork(true, false); + if (!mainIndexExists()) + { + return; + } + try + { + NodeRef lastId = null; + + toFTSIndex = new ArrayList(size); + BooleanQuery booleanQuery = new BooleanQuery(); + booleanQuery.add(new TermQuery(new Term("FTSSTATUS", "Dirty")), false, false); + booleanQuery.add(new TermQuery(new Term("FTSSTATUS", "New")), false, false); + + int count = 0; + Searcher searcher = null; + LuceneResultSet results = null; + try + { + searcher = getSearcher(null); + Hits hits; + try + { + hits = searcher.search(booleanQuery); + } + catch (IOException e) + { + throw new LuceneIndexException( + "Failed to execute query to find content which needs updating in the index", e); + } + results = new LuceneResultSet(hits, searcher, nodeService, null); + + for (ResultSetRow row : results) + { + LuceneResultSetRow lrow = (LuceneResultSetRow) row; + Helper helper = new Helper(lrow.getNodeRef(), lrow.getDocument().getField("TX").stringValue()); + toFTSIndex.add(helper); + if (++count >= size) + { + break; + } + } + count = results.length(); + } + finally + { + if (results != null) + { + results.close(); // closes the searcher + } + else if (searcher != null) + { + try + { + searcher.close(); + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to close searcher", e); + } + } + } + + if (toFTSIndex.size() > 0) + { + checkAbleToDoWork(true, true); + + IndexWriter writer = null; + try + { + writer = getDeltaWriter(); + for (Helper helper : toFTSIndex) + { + // Document document = helper.document; + NodeRef ref = helper.nodeRef; + + List docs = createDocuments(ref, false, true, false); + for (Document doc : docs) + { + try + { + writer.addDocument(doc /* + * TODO: Select the + * language based + * analyser + */); + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to add document while updating fts index", e); + } + } + + // Need to do all the current id in the TX - should all + // be + // together so skip until id changes + if (writer.docCount() > size) + { + if (lastId == null) + { + lastId = ref; + } + if (!lastId.equals(ref)) + { + break; + } + } + } + + remainingCount = count - writer.docCount(); + } + catch (LuceneIndexException e) + { + if (writer != null) + { + closeDeltaWriter(); + } + } + } + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Failed FTS update", e); + } + } + + public void registerCallBack(FTSIndexerAware callBack) + { + this.callBack = callBack; + } + + private static class Helper + { + NodeRef nodeRef; + + String tx; + + Helper(NodeRef nodeRef, String tx) + { + this.nodeRef = nodeRef; + this.tx = tx; + } + } + + private static class Command + { + NodeRef nodeRef; + + Action action; + + Command(NodeRef nodeRef, Action action) + { + this.nodeRef = nodeRef; + this.action = action; + } + + public String toString() + { + StringBuffer buffer = new StringBuffer(); + if (action == Action.INDEX) + { + buffer.append("Index "); + } + else if (action == Action.DELETE) + { + buffer.append("Delete "); + } + else if (action == Action.REINDEX) + { + buffer.append("Reindex "); + } + else + { + buffer.append("Unknown ... "); + } + buffer.append(nodeRef); + return buffer.toString(); + } + + } + + private FullTextSearchIndexer luceneFullTextSearchIndexer; + + public void setLuceneFullTextSearchIndexer(FullTextSearchIndexer luceneFullTextSearchIndexer) + { + this.luceneFullTextSearchIndexer = luceneFullTextSearchIndexer; + } + + public Set getDeletions() + { + return Collections.unmodifiableSet(deletions); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneQueryParser.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneQueryParser.java new file mode 100644 index 0000000000..8f2f6d70a4 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneQueryParser.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; +import java.io.StringReader; +import java.util.HashSet; + +import org.alfresco.repo.search.SearcherException; +import org.alfresco.repo.search.impl.lucene.query.PathQuery; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.apache.log4j.Logger; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.RangeQuery; +import org.apache.lucene.search.TermQuery; +import org.saxpath.SAXPathException; + +import com.werken.saxpath.XPathReader; + +public class LuceneQueryParser extends QueryParser +{ + private static Logger s_logger = Logger.getLogger(LuceneQueryParser.class); + + private NamespacePrefixResolver namespacePrefixResolver; + + private DictionaryService dictionaryService; + + /** + * Parses a query string, returning a {@link org.apache.lucene.search.Query}. + * + * @param query + * the query string to be parsed. + * @param field + * the default field for query terms. + * @param analyzer + * used to find terms in the query text. + * @throws ParseException + * if the parsing fails + */ + static public Query parse(String query, String field, Analyzer analyzer, + NamespacePrefixResolver namespacePrefixResolver, DictionaryService dictionaryService, int defaultOperator) + throws ParseException + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Using Alfresco Lucene Query Parser for query: " + query); + } + LuceneQueryParser parser = new LuceneQueryParser(field, analyzer); + parser.setOperator(defaultOperator); + parser.setNamespacePrefixResolver(namespacePrefixResolver); + parser.setDictionaryService(dictionaryService); + return parser.parse(query); + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public LuceneQueryParser(String arg0, Analyzer arg1) + { + super(arg0, arg1); + } + + public LuceneQueryParser(CharStream arg0) + { + super(arg0); + } + + public LuceneQueryParser(QueryParserTokenManager arg0) + { + super(arg0); + } + + protected Query getFieldQuery(String field, String queryText) throws ParseException + { + try + { + if (field.equals("PATH")) + { + XPathReader reader = new XPathReader(); + LuceneXPathHandler handler = new LuceneXPathHandler(); + handler.setNamespacePrefixResolver(namespacePrefixResolver); + handler.setDictionaryService(dictionaryService); + reader.setXPathHandler(handler); + reader.parse(queryText); + PathQuery pathQuery = handler.getQuery(); + pathQuery.setRepeats(false); + return pathQuery; + } + else if (field.equals("PATH_WITH_REPEATS")) + { + XPathReader reader = new XPathReader(); + LuceneXPathHandler handler = new LuceneXPathHandler(); + handler.setNamespacePrefixResolver(namespacePrefixResolver); + handler.setDictionaryService(dictionaryService); + reader.setXPathHandler(handler); + reader.parse(queryText); + PathQuery pathQuery = handler.getQuery(); + pathQuery.setRepeats(true); + return pathQuery; + } + else if (field.equals("ID")) + { + TermQuery termQuery = new TermQuery(new Term(field, queryText)); + return termQuery; + } + else if (field.equals("TX")) + { + TermQuery termQuery = new TermQuery(new Term(field, queryText)); + return termQuery; + } + else if (field.equals("PARENT")) + { + TermQuery termQuery = new TermQuery(new Term(field, queryText)); + return termQuery; + } + else if (field.equals("PRIMARYPARENT")) + { + TermQuery termQuery = new TermQuery(new Term(field, queryText)); + return termQuery; + } + else if (field.equals("QNAME")) + { + XPathReader reader = new XPathReader(); + LuceneXPathHandler handler = new LuceneXPathHandler(); + handler.setNamespacePrefixResolver(namespacePrefixResolver); + handler.setDictionaryService(dictionaryService); + reader.setXPathHandler(handler); + reader.parse("//" + queryText); + return handler.getQuery(); + } + else if (field.equals("TYPE")) + { + TypeDefinition target = dictionaryService.getType(QName.createQName(queryText)); + if (target == null) + { + throw new SearcherException("Invalid type: " + queryText); + } + QName targetQName = target.getName(); + HashSet subclasses = new HashSet(); + for (QName classRef : dictionaryService.getAllTypes()) + { + TypeDefinition current = dictionaryService.getType(classRef); + while ((current != null) && !current.getName().equals(targetQName)) + { + current = (current.getParentName() == null) ? null : dictionaryService.getType(current + .getParentName()); + } + if (current != null) + { + subclasses.add(classRef); + } + } + BooleanQuery booleanQuery = new BooleanQuery(); + for (QName qname : subclasses) + { + TermQuery termQuery = new TermQuery(new Term(field, qname.toString())); + booleanQuery.add(termQuery, false, false); + } + return booleanQuery; + } + else if (field.equals("ASPECT")) + { + AspectDefinition target = dictionaryService.getAspect(QName.createQName(queryText)); + QName targetQName = target.getName(); + HashSet subclasses = new HashSet(); + for (QName classRef : dictionaryService.getAllAspects()) + { + AspectDefinition current = dictionaryService.getAspect(classRef); + while ((current != null) && !current.getName().equals(targetQName)) + { + current = (current.getParentName() == null) ? null : dictionaryService.getAspect(current + .getParentName()); + } + if (current != null) + { + subclasses.add(classRef); + } + } + + BooleanQuery booleanQuery = new BooleanQuery(); + for (QName qname : subclasses) + { + TermQuery termQuery = new TermQuery(new Term(field, qname.toString())); + booleanQuery.add(termQuery, false, false); + } + return booleanQuery; + } + else if (field.startsWith("@")) + { + + String expandedFieldName = field; + // Check for any prefixes and expand to the full uri + if (field.charAt(1) != '{') + { + int colonPosition = field.indexOf(':'); + if (colonPosition == -1) + { + // use the default namespace + expandedFieldName = "@{" + + namespacePrefixResolver.getNamespaceURI("") + "}" + field.substring(1); + } + else + { + // find the prefix + expandedFieldName = "@{" + + namespacePrefixResolver.getNamespaceURI(field.substring(1, colonPosition)) + "}" + + field.substring(colonPosition + 1); + } + } + + if(expandedFieldName.endsWith(".mimetype")) + { + QName propertyQName = QName.createQName(expandedFieldName.substring(1, expandedFieldName.length()-9)); + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + if((propertyDef != null) && (propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT))) + { + TermQuery termQuery = new TermQuery(new Term(expandedFieldName, queryText)); + return termQuery; + } + + } + + // Already in expanded form + return super.getFieldQuery(expandedFieldName, queryText); + + + } + else + { + return super.getFieldQuery(field, queryText); + } + } + catch (SAXPathException e) + { + throw new ParseException("Failed to parse XPath...\n" + e.getMessage()); + } + + } + + /** + * @exception ParseException + * throw in overridden method to disallow + */ + protected Query getRangeQuery(String field, String part1, String part2, boolean inclusive) throws ParseException + { + if (field.startsWith("@")) + { + String fieldName = field; + // Check for any prefixes and expand to the full uri + if (field.charAt(1) != '{') + { + int colonPosition = field.indexOf(':'); + if (colonPosition == -1) + { + // use the default namespace + fieldName = "@{" + namespacePrefixResolver.getNamespaceURI("") + "}" + field.substring(1); + } + else + { + // find the prefix + fieldName = "@{" + + namespacePrefixResolver.getNamespaceURI(field.substring(1, colonPosition)) + "}" + + field.substring(colonPosition + 1); + } + } + return new RangeQuery(new Term(fieldName, getToken(fieldName, part1)), new Term(fieldName, getToken( + fieldName, part2)), inclusive); + + } + else + { + return super.getRangeQuery(field, part1, part2, inclusive); + } + + } + + private String getToken(String field, String value) + { + TokenStream source = analyzer.tokenStream(field, new StringReader(value)); + org.apache.lucene.analysis.Token t; + String tokenised = null; + + while (true) + { + try + { + t = source.next(); + } + catch (IOException e) + { + t = null; + } + if (t == null) + break; + tokenised = t.termText(); + } + try + { + source.close(); + } + catch (IOException e) + { + + } + return tokenised; + + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSet.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSet.java new file mode 100644 index 0000000000..23b87527f0 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSet.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; + +import org.alfresco.repo.search.AbstractResultSet; +import org.alfresco.repo.search.ResultSetRowIterator; +import org.alfresco.repo.search.SearcherException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.Hits; +import org.apache.lucene.search.Searcher; + +/** + * Implementation of a ResultSet on top of Lucene Hits class. + * + * @author andyh + * + */ +public class LuceneResultSet extends AbstractResultSet +{ + /** + * The underlying hits + */ + Hits hits; + + private Searcher searcher; + + private NodeService nodeService; + + /** + * Wrap a lucene seach result with node support + * + * @param storeRef + * @param hits + */ + public LuceneResultSet(Hits hits, Searcher searcher, NodeService nodeService, Path[]propertyPaths) + { + super(propertyPaths); + this.hits = hits; + this.searcher = searcher; + this.nodeService = nodeService; + } + + /* + * ResultSet implementation + */ + + public ResultSetRowIterator iterator() + { + return new LuceneResultSetRowIterator(this); + } + + public int length() + { + return hits.length(); + } + + public NodeRef getNodeRef(int n) + { + try + { + // We have to get the document to resolve this + // It is possible the store ref is also stored in the index + Document doc = hits.doc(n); + String id = doc.get("ID"); + return new NodeRef(id); + } + catch (IOException e) + { + throw new SearcherException("IO Error reading reading node ref from the result set", e); + } + } + + public float getScore(int n) throws SearcherException + { + try + { + return hits.score(n); + } + catch (IOException e) + { + throw new SearcherException("IO Error reading score from the result set", e); + } + } + + public Document getDocument(int n) + { + try + { + Document doc = hits.doc(n); + return doc; + } + catch (IOException e) + { + throw new SearcherException("IO Error reading reading document from the result set", e); + } + } + + public void close() + { + try + { + searcher.close(); + } + catch (IOException e) + { + throw new SearcherException(e); + } + } + + public NodeService getNodeService() + { + return nodeService; + } + + public ResultSetRow getRow(int i) + { + if(i < length()) + { + return new LuceneResultSetRow(this, i); + } + else + { + throw new SearcherException("Invalid row"); + } + } + + public ChildAssociationRef getChildAssocRef(int n) + { + return getRow(n).getChildAssocRef(); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRow.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRow.java new file mode 100644 index 0000000000..3a18f51d6a --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRow.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.AbstractResultSetRow; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; + +/** + * A row in a result set. Created on the fly. + * + * @author Andy Hind + * + */ +public class LuceneResultSetRow extends AbstractResultSetRow +{ + /** + * The current document - cached so we do not get it for each value + */ + private Document document; + + /** + * Wrap a position in a lucene Hits class with node support + * + * @param resultSet + * @param position + */ + public LuceneResultSetRow(LuceneResultSet resultSet, int index) + { + super(resultSet, index); + } + + /** + * Support to cache the document for this row + * + * @return + */ + public Document getDocument() + { + if (document == null) + { + document = ((LuceneResultSet) getResultSet()).getDocument(getIndex()); + } + return document; + } + + /* + * ResultSetRow implementation + */ + + protected Map getDirectProperties() + { + LuceneResultSet lrs = (LuceneResultSet) getResultSet(); + return lrs.getNodeService().getProperties(lrs.getNodeRef(getIndex())); + } + + public Serializable getValue(Path path) + { + // TODO: implement path base look up against the document or via the + // node service + throw new UnsupportedOperationException(); + } + + public QName getQName() + { + Field field = getDocument().getField("QNAME"); + if (field != null) + { + String qname = field.stringValue(); + if((qname == null) || (qname.length() == 0)) + { + return null; + } + else + { + return QName.createQName(qname); + } + } + else + { + return null; + } + } + + public QName getPrimaryAssocTypeQName() + { + + Field field = getDocument().getField("PRIMARYASSOCTYPEQNAME"); + if (field != null) + { + String qname = field.stringValue(); + return QName.createQName(qname); + } + else + { + return ContentModel.ASSOC_CHILDREN; + } + } + + public ChildAssociationRef getChildAssocRef() + { + Field field = getDocument().getField("PRIMARYPARENT"); + String primaryParent = null; + if (field != null) + { + primaryParent = field.stringValue(); + } + NodeRef childNodeRef = getNodeRef(); + NodeRef parentNodeRef = primaryParent == null ? null : new NodeRef(primaryParent); + return new ChildAssociationRef(getPrimaryAssocTypeQName(), parentNodeRef, getQName(), childNodeRef); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRowIterator.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRowIterator.java new file mode 100644 index 0000000000..248609bde6 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneResultSetRowIterator.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.alfresco.repo.search.AbstractResultSetRowIterator; +import org.alfresco.service.cmr.search.ResultSetRow; + +/** + * Iterate over the rows in a LuceneResultSet + * + * @author andyh + * + */ +public class LuceneResultSetRowIterator extends AbstractResultSetRowIterator +{ + /** + * Create an iterator over the result set. Follows standard ListIterator + * conventions + * + * @param resultSet + */ + public LuceneResultSetRowIterator(LuceneResultSet resultSet) + { + super(resultSet); + } + + public ResultSetRow next() + { + return new LuceneResultSetRow((LuceneResultSet)getResultSet(), moveToNextPosition()); + } + + public ResultSetRow previous() + { + return new LuceneResultSetRow((LuceneResultSet)getResultSet(), moveToPreviousPosition()); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcher.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcher.java new file mode 100644 index 0000000000..425583e12d --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcher.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; + +public interface LuceneSearcher extends SearchService, Lockable +{ + public boolean indexExists(); + public void setNodeService(NodeService nodeService); + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver); +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl.java new file mode 100644 index 0000000000..8191629b4c --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl.java @@ -0,0 +1,652 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.search.CannedQueryDef; +import org.alfresco.repo.search.EmptyResultSet; +import org.alfresco.repo.search.ISO9075; +import org.alfresco.repo.search.Indexer; +import org.alfresco.repo.search.QueryRegisterComponent; +import org.alfresco.repo.search.SearcherException; +import org.alfresco.repo.search.impl.NodeSearcher; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.XPathException; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.QueryParameter; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.SearchLanguageConversion; +import org.apache.lucene.search.Hits; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.saxpath.SAXPathException; + +import com.werken.saxpath.XPathReader; + +/** + * The Lucene implementation of Searcher At the moment we support only lucene + * based queries. + * + * TODO: Support for other query languages + * + * @author andyh + * + */ +public class LuceneSearcherImpl extends LuceneBase implements LuceneSearcher +{ + /** + * Default field name + */ + private static final String DEFAULT_FIELD = "TEXT"; + + private NamespacePrefixResolver namespacePrefixResolver; + + private NodeService nodeService; + + private DictionaryService dictionaryService; + + private QueryRegisterComponent queryRegister; + + private LuceneIndexer indexer; + + /* + * Searcher implementation + */ + + /** + * Get an initialised searcher for the store and transaction Normally we do + * not search against a a store and delta. Currently only gets the searcher + * against the main index. + * + * @param storeRef + * @param deltaId + * @return + */ + public static LuceneSearcherImpl getSearcher(StoreRef storeRef, LuceneIndexer indexer, LuceneConfig config) + { + LuceneSearcherImpl searcher = new LuceneSearcherImpl(); + searcher.setLuceneConfig(config); + try + { + searcher.initialise(storeRef, indexer == null ? null : indexer.getDeltaId(), false, false); + searcher.indexer = indexer; + } + catch (LuceneIndexException e) + { + throw new SearcherException(e); + } + return searcher; + } + + /** + * Get an intialised searcher for the store. No transactional ammendsmends + * are searched. + * + * + * @param storeRef + * @return + */ + public static LuceneSearcherImpl getSearcher(StoreRef storeRef, LuceneConfig config) + { + return getSearcher(storeRef, null, config); + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public boolean indexExists() + { + return mainIndexExists(); + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setQueryRegister(QueryRegisterComponent queryRegister) + { + this.queryRegister = queryRegister; + } + + public ResultSet query(StoreRef store, String language, String queryString, Path[] queryOptions, + QueryParameterDefinition[] queryParameterDefinitions) throws SearcherException + { + SearchParameters sp = new SearchParameters(); + sp.addStore(store); + sp.setLanguage(language); + sp.setQuery(queryString); + if (queryOptions != null) + { + for (Path path : queryOptions) + { + sp.addAttrbutePath(path); + } + } + if (queryParameterDefinitions != null) + { + for (QueryParameterDefinition qpd : queryParameterDefinitions) + { + sp.addQueryParameterDefinition(qpd); + } + } + sp.excludeDataInTheCurrentTransaction(true); + + return query(sp); + } + + public ResultSet query(SearchParameters searchParameters) + { + if (searchParameters.getStores().size() != 1) + { + throw new IllegalStateException("Only one store can be searched at present"); + } + + String parameterisedQueryString; + if (searchParameters.getQueryParameterDefinitions().size() > 0) + { + Map map = new HashMap(); + + for (QueryParameterDefinition qpd : searchParameters.getQueryParameterDefinitions()) + { + map.put(qpd.getQName(), qpd); + } + + parameterisedQueryString = parameterise(searchParameters.getQuery(), map, null, namespacePrefixResolver); + } + else + { + parameterisedQueryString = searchParameters.getQuery(); + } + + if (searchParameters.getLanguage().equalsIgnoreCase(SearchService.LANGUAGE_LUCENE)) + { + try + { + + int defaultOperator; + if (searchParameters.getDefaultOperator() == SearchParameters.AND) + { + defaultOperator = LuceneQueryParser.DEFAULT_OPERATOR_AND; + } + else + { + defaultOperator = LuceneQueryParser.DEFAULT_OPERATOR_OR; + } + + Query query = LuceneQueryParser.parse(parameterisedQueryString, DEFAULT_FIELD, new LuceneAnalyser( + dictionaryService), namespacePrefixResolver, dictionaryService, defaultOperator); + Searcher searcher = getSearcher(indexer); + if (searcher == null) + { + // no index return an empty result set + return new EmptyResultSet(); + } + + Hits hits; + + if (searchParameters.getSortDefinitions().size() > 0) + { + int index = 0; + SortField[] fields = new SortField[searchParameters.getSortDefinitions().size()]; + for (SearchParameters.SortDefinition sd : searchParameters.getSortDefinitions()) + { + switch (sd.getSortType()) + { + case FIELD: + fields[index++] = new SortField(sd.getField(), !sd.isAscending()); + break; + case DOCUMENT: + fields[index++] = new SortField(null, SortField.DOC, !sd.isAscending()); + break; + case SCORE: + fields[index++] = new SortField(null, SortField.SCORE, !sd.isAscending()); + break; + } + + } + hits = searcher.search(query, new Sort(fields)); + } + else + { + hits = searcher.search(query); + } + + return new LuceneResultSet(hits, searcher, nodeService, searchParameters.getAttributePaths().toArray( + new Path[0])); + + } + catch (ParseException e) + { + throw new SearcherException("Failed to parse query: " + parameterisedQueryString, e); + } + catch (IOException e) + { + throw new SearcherException("IO exception during search", e); + } + } + else if (searchParameters.getLanguage().equalsIgnoreCase(SearchService.LANGUAGE_XPATH)) + { + try + { + XPathReader reader = new XPathReader(); + LuceneXPathHandler handler = new LuceneXPathHandler(); + handler.setNamespacePrefixResolver(namespacePrefixResolver); + handler.setDictionaryService(dictionaryService); + // TODO: Handler should have the query parameters to use in + // building its lucene query + // At the moment xpath style parameters in the PATH + // expression are not supported. + reader.setXPathHandler(handler); + reader.parse(parameterisedQueryString); + Query query = handler.getQuery(); + Searcher searcher = getSearcher(null); + if (searcher == null) + { + // no index return an empty result set + return new EmptyResultSet(); + } + Hits hits = searcher.search(query); + return new LuceneResultSet(hits, searcher, nodeService, searchParameters.getAttributePaths().toArray( + new Path[0])); + } + catch (SAXPathException e) + { + throw new SearcherException("Failed to parse query: " + searchParameters.getQuery(), e); + } + catch (IOException e) + { + throw new SearcherException("IO exception during search", e); + } + } + else + { + throw new SearcherException("Unknown query language: " + searchParameters.getLanguage()); + } + } + + public ResultSet query(StoreRef store, String language, String query) + { + return query(store, language, query, null, null); + } + + public ResultSet query(StoreRef store, String language, String query, + QueryParameterDefinition[] queryParameterDefintions) + { + return query(store, language, query, null, queryParameterDefintions); + } + + public ResultSet query(StoreRef store, String language, String query, Path[] attributePaths) + { + return query(store, language, query, attributePaths, null); + } + + public ResultSet query(StoreRef store, QName queryId, QueryParameter[] queryParameters) + { + CannedQueryDef definition = queryRegister.getQueryDefinition(queryId); + + // Do parameter replacement + // As lucene phrases are tokensied it is correct to just do straight + // string replacement. + // The string will be formatted by the tokeniser. + // + // For non phrase queries this is incorrect but string replacement is + // probably the best we can do. + // As numbers and text are indexed specially, direct term queries only + // make sense against textual data + + checkParameters(definition, queryParameters); + + String queryString = parameterise(definition.getQuery(), definition.getQueryParameterMap(), queryParameters, + definition.getNamespacePrefixResolver()); + + return query(store, definition.getLanguage(), queryString, null, null); + } + + /** + * The definitions must provide a default value, or of not there must be a + * parameter to provide the value + * + * @param definition + * @param queryParameters + * @throws QueryParameterisationException + */ + private void checkParameters(CannedQueryDef definition, QueryParameter[] queryParameters) + throws QueryParameterisationException + { + List missing = new ArrayList(); + + Set parameterQNameSet = new HashSet(); + if (queryParameters != null) + { + for (QueryParameter parameter : queryParameters) + { + parameterQNameSet.add(parameter.getQName()); + } + } + + for (QueryParameterDefinition parameterDefinition : definition.getQueryParameterDefs()) + { + if (!parameterDefinition.hasDefaultValue()) + { + if (!parameterQNameSet.contains(parameterDefinition.getQName())) + { + missing.add(parameterDefinition.getQName()); + } + } + } + + if (missing.size() > 0) + { + StringBuilder buffer = new StringBuilder(128); + buffer.append("The query is missing values for the following parameters: "); + for (QName qName : missing) + { + buffer.append(qName); + buffer.append(", "); + } + buffer.delete(buffer.length() - 1, buffer.length() - 1); + buffer.delete(buffer.length() - 1, buffer.length() - 1); + throw new QueryParameterisationException(buffer.toString()); + } + } + + /* + * Parameterise the query string - not sure if it is required to escape + * lucence spacials chars The parameters could be used to build the query - + * the contents of parameters should alread have been escaped if required. + * ... mush better to provide the parameters and work out what to do TODO: + * conditional query escapement - may be we should have a parameter type + * that is not escaped + */ + private String parameterise(String unparameterised, Map map, + QueryParameter[] queryParameters, NamespacePrefixResolver nspr) throws QueryParameterisationException + { + + Map> valueMap = new HashMap>(); + + if (queryParameters != null) + { + for (QueryParameter parameter : queryParameters) + { + List list = valueMap.get(parameter.getQName()); + if (list == null) + { + list = new ArrayList(); + valueMap.put(parameter.getQName(), list); + } + list.add(parameter.getValue()); + } + } + + Map> iteratorMap = new HashMap>(); + + List missing = new ArrayList(1); + StringBuilder buffer = new StringBuilder(unparameterised); + int index = 0; + while ((index = buffer.indexOf("${", index)) != -1) + { + int endIndex = buffer.indexOf("}", index); + String qNameString = buffer.substring(index + 2, endIndex); + QName key = QName.createQName(qNameString, nspr); + QueryParameterDefinition parameterDefinition = map.get(key); + if (parameterDefinition == null) + { + missing.add(key); + buffer.replace(index, endIndex + 1, ""); + } + else + { + ListIterator it = iteratorMap.get(key); + if ((it == null) || (!it.hasNext())) + { + List list = valueMap.get(key); + if ((list != null) && (list.size() > 0)) + { + it = list.listIterator(); + } + if (it != null) + { + iteratorMap.put(key, it); + } + } + String value; + if (it == null) + { + value = parameterDefinition.getDefault(); + } + else + { + value = DefaultTypeConverter.INSTANCE.convert(String.class, it.next()); + } + buffer.replace(index, endIndex + 1, value); + } + } + if (missing.size() > 0) + { + StringBuilder error = new StringBuilder(); + error.append("The query uses the following parameters which are not defined: "); + for (QName qName : missing) + { + error.append(qName); + error.append(", "); + } + error.delete(error.length() - 1, error.length() - 1); + error.delete(error.length() - 1, error.length() - 1); + throw new QueryParameterisationException(error.toString()); + } + return buffer.toString(); + } + + /** + * @see org.alfresco.repo.search.impl.NodeSearcher + */ + public List selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, + NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks, String language) + throws InvalidNodeRefException, XPathException + { + NodeSearcher nodeSearcher = new NodeSearcher(nodeService, dictionaryService, this); + return nodeSearcher.selectNodes(contextNodeRef, xpath, parameters, namespacePrefixResolver, + followAllParentLinks, language); + } + + /** + * @see org.alfresco.repo.search.impl.NodeSearcher + */ + public List selectProperties(NodeRef contextNodeRef, String xpath, + QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks, String language) throws InvalidNodeRefException, XPathException + { + NodeSearcher nodeSearcher = new NodeSearcher(nodeService, dictionaryService, this); + return nodeSearcher.selectProperties(contextNodeRef, xpath, parameters, namespacePrefixResolver, + followAllParentLinks, language); + } + + /** + * @return Returns true if the pattern is present, otherwise false. + */ + public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern) + { + return contains(nodeRef, propertyQName, googleLikePattern, SearchParameters.Operator.OR); + } + + /** + * @return Returns true if the pattern is present, otherwise false. + */ + public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern, + SearchParameters.Operator defaultOperator) + { + ResultSet resultSet = null; + try + { + // build Lucene search string specific to the node + StringBuilder sb = new StringBuilder(); + sb.append("+ID:\"").append(nodeRef.toString()).append("\" +(TEXT:(") + .append(googleLikePattern.toLowerCase()).append(") "); + if (propertyQName != null) + { + sb.append(" OR @").append( + LuceneQueryParser.escape(QName.createQName(propertyQName.getNamespaceURI(), + ISO9075.encode(propertyQName.getLocalName())).toString())); + sb.append(":(").append(googleLikePattern.toLowerCase()).append(")"); + } + else + { + for (QName key : nodeService.getProperties(nodeRef).keySet()) + { + sb.append(" OR @").append( + LuceneQueryParser.escape(QName.createQName(key.getNamespaceURI(), + ISO9075.encode(key.getLocalName())).toString())); + sb.append(":(").append(googleLikePattern.toLowerCase()).append(")"); + } + } + sb.append(")"); + + SearchParameters sp = new SearchParameters(); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(sb.toString()); + sp.setDefaultOperator(defaultOperator); + sp.addStore(nodeRef.getStoreRef()); + + resultSet = this.query(sp); + boolean answer = resultSet.length() > 0; + return answer; + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + } + + /** + * @return Returns true if the pattern is present, otherwise false. + * + * @see #setIndexer(Indexer) + * @see #setSearcher(SearchService) + */ + public boolean like(NodeRef nodeRef, QName propertyQName, String sqlLikePattern, boolean includeFTS) + { + if (propertyQName == null) + { + throw new IllegalArgumentException("Property QName is mandatory for the like expression"); + } + + StringBuilder sb = new StringBuilder(sqlLikePattern.length() * 3); + + if (includeFTS) + { + // convert the SQL-like pattern into a Lucene-compatible string + String pattern = SearchLanguageConversion.convertXPathLikeToLucene(sqlLikePattern.toLowerCase()); + + // build Lucene search string specific to the node + sb = new StringBuilder(); + sb.append("+ID:\"").append(nodeRef.toString()).append("\" +("); + // FTS or attribute matches + if (includeFTS) + { + sb.append("TEXT:(").append(pattern).append(") "); + } + if (propertyQName != null) + { + sb.append(" @").append( + LuceneQueryParser.escape(QName.createQName(propertyQName.getNamespaceURI(), + ISO9075.encode(propertyQName.getLocalName())).toString())).append(":(").append(pattern) + .append(")"); + } + sb.append(")"); + + ResultSet resultSet = null; + try + { + resultSet = this.query(nodeRef.getStoreRef(), "lucene", sb.toString()); + boolean answer = resultSet.length() > 0; + return answer; + } + finally + { + if (resultSet != null) + { + resultSet.close(); + } + } + } + else + { + // convert the SQL-like pattern into a Lucene-compatible string + String pattern = SearchLanguageConversion.convertXPathLikeToRegex(sqlLikePattern.toLowerCase()); + + Serializable property = nodeService.getProperty(nodeRef, propertyQName); + if (property == null) + { + return false; + } + else + { + String propertyString = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty( + nodeRef, propertyQName)); + return propertyString.toLowerCase().matches(pattern); + } + } + } + + public List selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, + NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + throws InvalidNodeRefException, XPathException + { + return selectNodes(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks, + SearchService.LANGUAGE_XPATH); + } + + public List selectProperties(NodeRef contextNodeRef, String xpath, + QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks) throws InvalidNodeRefException, XPathException + { + return selectProperties(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks, + SearchService.LANGUAGE_XPATH); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneTest.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneTest.java new file mode 100644 index 0000000000..cc6338afd1 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneTest.java @@ -0,0 +1,3133 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.repo.search.ISO9075; +import org.alfresco.repo.search.QueryParameterDefImpl; +import org.alfresco.repo.search.QueryRegisterComponent; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.search.results.ChildAssocRefResultSet; +import org.alfresco.repo.search.results.DetachedResultSet; +import org.alfresco.repo.search.transaction.LuceneIndexLock; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.QueryParameter; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.CachingDateFormat; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; + +/** + * @author andyh + * + */ +public class LuceneTest extends TestCase +{ + private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/lucenetest"; + + private static final QName ASSOC_TYPE_QNAME = QName.createQName(TEST_NAMESPACE, "assoc"); + + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private static Log logger = LogFactory.getLog(LuceneTest.class); + + TransactionService transactionService; + + NodeService nodeService; + + DictionaryService dictionaryService; + + LuceneIndexLock luceneIndexLock; + + private NodeRef rootNodeRef; + + private NodeRef n1; + + private NodeRef n2; + + private NodeRef n3; + + private NodeRef n4; + + private NodeRef n5; + + private NodeRef n6; + + private NodeRef n7; + + private NodeRef n8; + + private NodeRef n9; + + private NodeRef n10; + + private NodeRef n11; + + private NodeRef n12; + + private NodeRef n13; + + private NodeRef n14; + + private DictionaryDAO dictionaryDAO; + + private FullTextSearchIndexer luceneFTS; + + private QName testType = QName.createQName(TEST_NAMESPACE, "testType"); + + private QName testSuperType = QName.createQName(TEST_NAMESPACE, "testSuperType"); + + private QName testAspect = QName.createQName(TEST_NAMESPACE, "testAspect"); + + private QName testSuperAspect = QName.createQName(TEST_NAMESPACE, "testSuperAspect"); + + private ContentService contentService; + + private QueryRegisterComponent queryRegisterComponent; + + private NamespacePrefixResolver namespacePrefixResolver; + + private LuceneIndexerAndSearcher indexerAndSearcher; + + private ServiceRegistry serviceRegistry; + + private UserTransaction testTX; + + private AuthenticationComponent authenticationComponent; + + private NodeRef[] documentOrder; + + public LuceneTest() + { + super(); + } + + public void setUp() throws Exception + { + nodeService = (NodeService) ctx.getBean("dbNodeService"); + luceneIndexLock = (LuceneIndexLock) ctx.getBean("luceneIndexLock"); + dictionaryService = (DictionaryService) ctx.getBean("dictionaryService"); + dictionaryDAO = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + luceneFTS = (FullTextSearchIndexer) ctx.getBean("LuceneFullTextSearchIndexer"); + contentService = (ContentService) ctx.getBean("contentService"); + queryRegisterComponent = (QueryRegisterComponent) ctx.getBean("queryRegisterComponent"); + namespacePrefixResolver = (NamespacePrefixResolver) ctx.getBean("namespaceService"); + indexerAndSearcher = (LuceneIndexerAndSearcher) ctx.getBean("luceneIndexerAndSearcherFactory"); + transactionService = (TransactionService) ctx.getBean("transactionComponent"); + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + this.authenticationComponent.setSystemUserAsCurrentUser(); + + queryRegisterComponent.loadQueryCollection("testQueryRegister.xml"); + + assertEquals(true, ctx.isSingleton("luceneIndexLock")); + assertEquals(true, ctx.isSingleton("LuceneFullTextSearchIndexer")); + + testTX = transactionService.getUserTransaction(); + testTX.begin(); + + // load in the test model + ClassLoader cl = BaseNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("org/alfresco/repo/search/impl/lucene/LuceneTest_model.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDAO.putModel(model); + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + + n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}one"), + testSuperType).getChildRef(); + nodeService.setProperty(n1, QName.createQName("{namespace}property-1"), "ValueOne"); + + n2 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}two"), + testSuperType).getChildRef(); + nodeService.setProperty(n2, QName.createQName("{namespace}property-1"), "valueone"); + nodeService.setProperty(n2, QName.createQName("{namespace}property-2"), "valuetwo"); + + + n3 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}three"), + testSuperType).getChildRef(); + + ObjectOutputStream oos; + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(n3); + oos.close(); + + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())); + Object o = ois.readObject(); + ois.close(); + } + catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + catch (ClassNotFoundException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + Map testProperties = new HashMap(); + testProperties.put(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-atomic"), + "TEXT THAT IS INDEXED STORED AND TOKENISED ATOMICALLY KEYONE"); + testProperties.put(QName.createQName(TEST_NAMESPACE, "text-indexed-unstored-tokenised-atomic"), + "TEXT THAT IS INDEXED STORED AND TOKENISED ATOMICALLY KEYUNSTORED"); + testProperties.put(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-nonatomic"), + "TEXT THAT IS INDEXED STORED AND TOKENISED BUT NOT ATOMICALLY KEYTWO"); + testProperties.put(QName.createQName(TEST_NAMESPACE, "int-ista"), Integer.valueOf(1)); + testProperties.put(QName.createQName(TEST_NAMESPACE, "long-ista"), Long.valueOf(2)); + testProperties.put(QName.createQName(TEST_NAMESPACE, "float-ista"), Float.valueOf(3.4f)); + testProperties.put(QName.createQName(TEST_NAMESPACE, "double-ista"), Double.valueOf(5.6)); + testProperties.put(QName.createQName(TEST_NAMESPACE, "date-ista"), new Date()); + testProperties.put(QName.createQName(TEST_NAMESPACE, "datetime-ista"), new Date()); + testProperties.put(QName.createQName(TEST_NAMESPACE, "boolean-ista"), Boolean.valueOf(true)); + testProperties.put(QName.createQName(TEST_NAMESPACE, "qname-ista"), QName.createQName("{wibble}wobble")); + testProperties.put(QName.createQName(TEST_NAMESPACE, "category-ista"), new NodeRef(storeRef, "CategoryId")); + testProperties.put(QName.createQName(TEST_NAMESPACE, "noderef-ista"), n1); + testProperties.put(QName.createQName(TEST_NAMESPACE, "path-ista"), nodeService.getPath(n3)); + testProperties.put(QName.createQName(TEST_NAMESPACE, "null"), null); + testProperties.put(QName.createQName(TEST_NAMESPACE, "list"), new ArrayList()); + ArrayList testList = new ArrayList(); + testList.add(null); + testProperties.put(QName.createQName(TEST_NAMESPACE, "nullList"), testList); + ArrayList testList2 = new ArrayList(); + testList2.add("woof"); + testList2.add(null); + + n4 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}four"), + testType, testProperties).getChildRef(); + + nodeService.getProperties(n1); + nodeService.getProperties(n2); + nodeService.getProperties(n3); + nodeService.getProperties(n4); + + n5 = nodeService.createNode(n1, ASSOC_TYPE_QNAME, QName.createQName("{namespace}five"), testSuperType) + .getChildRef(); + n6 = nodeService.createNode(n1, ASSOC_TYPE_QNAME, QName.createQName("{namespace}six"), testSuperType) + .getChildRef(); + n7 = nodeService.createNode(n2, ASSOC_TYPE_QNAME, QName.createQName("{namespace}seven"), testSuperType) + .getChildRef(); + n8 = nodeService.createNode(n2, ASSOC_TYPE_QNAME, QName.createQName("{namespace}eight-2"), testSuperType) + .getChildRef(); + n9 = nodeService.createNode(n5, ASSOC_TYPE_QNAME, QName.createQName("{namespace}nine"), testSuperType) + .getChildRef(); + n10 = nodeService.createNode(n5, ASSOC_TYPE_QNAME, QName.createQName("{namespace}ten"), testSuperType) + .getChildRef(); + n11 = nodeService.createNode(n5, ASSOC_TYPE_QNAME, QName.createQName("{namespace}eleven"), testSuperType) + .getChildRef(); + n12 = nodeService.createNode(n5, ASSOC_TYPE_QNAME, QName.createQName("{namespace}twelve"), testSuperType) + .getChildRef(); + n13 = nodeService.createNode(n12, ASSOC_TYPE_QNAME, QName.createQName("{namespace}thirteen"), testSuperType) + .getChildRef(); + + Map properties = new HashMap(); + properties.put(ContentModel.PROP_CONTENT, new ContentData(null, "text/plain", 0L, "UTF-16")); + n14 = nodeService.createNode(n13, ASSOC_TYPE_QNAME, QName.createQName("{namespace}fourteen"), + ContentModel.TYPE_CONTENT, properties).getChildRef(); + // nodeService.addAspect(n14, DictionaryBootstrap.ASPECT_QNAME_CONTENT, + // properties); + + ContentWriter writer = contentService.getWriter(n14, ContentModel.PROP_CONTENT, true); + // InputStream is = + // this.getClass().getClassLoader().getResourceAsStream("test.doc"); + // writer.putContent(is); + writer.putContent("The quick brown fox jumped over the lazy dog"); + + nodeService.addChild(rootNodeRef, n8, ContentModel.ASSOC_CHILDREN, QName.createQName("{namespace}eight-0")); + nodeService.addChild(n1, n8, ASSOC_TYPE_QNAME, QName.createQName("{namespace}eight-1")); + nodeService.addChild(n2, n13, ASSOC_TYPE_QNAME, QName.createQName("{namespace}link")); + + nodeService.addChild(n1, n14, ASSOC_TYPE_QNAME, QName.createQName("{namespace}common")); + nodeService.addChild(n2, n14, ASSOC_TYPE_QNAME, QName.createQName("{namespace}common")); + nodeService.addChild(n5, n14, ASSOC_TYPE_QNAME, QName.createQName("{namespace}common")); + nodeService.addChild(n6, n14, ASSOC_TYPE_QNAME, QName.createQName("{namespace}common")); + nodeService.addChild(n12, n14, ASSOC_TYPE_QNAME, QName.createQName("{namespace}common")); + nodeService.addChild(n13, n14, ASSOC_TYPE_QNAME, QName.createQName("{namespace}common")); + + documentOrder= new NodeRef[]{rootNodeRef, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14}; + + } + + @Override + protected void tearDown() throws Exception + { + + if (testTX.getStatus() == Status.STATUS_ACTIVE) + { + testTX.rollback(); + } + authenticationComponent.clearCurrentSecurityContext(); + super.tearDown(); + } + + public LuceneTest(String arg0) + { + super(arg0); + } + + + public void test0() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + luceneFTS.resume(); + } + + + + public void testDeleteIssue() throws Exception + { + + testTX.commit(); + + + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + ChildAssociationRef testFind = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}testFind"), testSuperType); + tx.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setQueryRegister(queryRegisterComponent); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "QNAME:\"namespace:testFind\""); + assertEquals(1, results.length()); + results.close(); + + UserTransaction tx1 = transactionService.getUserTransaction(); + tx1.begin(); + for (int i = 0; i < 100; i++) + { + HashSet refs = new HashSet(); + for (int j =0 ; j < i; j++) + { + ChildAssociationRef test = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}test"), testSuperType); + refs.add(test); + } + + for(ChildAssociationRef car : refs) + { + nodeService.deleteNode(car.getChildRef()); + } + + } + tx1.commit(); + + UserTransaction tx3 = transactionService.getUserTransaction(); + tx3.begin(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "QNAME:\"namespace:testFind\""); + assertEquals(1, results.length()); + results.close(); + tx3.commit(); + } + + + public void testMTDeleteIssue() throws Exception + { + + testTX.commit(); + + + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + ChildAssociationRef testFind = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}testFind"), testSuperType); + tx.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setQueryRegister(queryRegisterComponent); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "QNAME:\"namespace:testFind\""); + assertEquals(1, results.length()); + results.close(); + + + Thread runner = null; + + for (int i = 0; i < 20; i++) + { + runner = new Nester("Concurrent-" + i, runner); + } + if (runner != null) + { + runner.start(); + + try + { + runner.join(); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + + + UserTransaction tx3 = transactionService.getUserTransaction(); + tx3.begin(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "QNAME:\"namespace:testFind\""); + assertEquals(1, results.length()); + results.close(); + tx3.commit(); + } + + class Nester extends Thread + { + Thread waiter; + + Nester(String name, Thread waiter) + { + super(name); + this.setDaemon(true); + this.waiter = waiter; + } + + public void run() + { + authenticationComponent.setSystemUserAsCurrentUser(); + if (waiter != null) + { + waiter.start(); + } + try + { + System.out.println("Start " + this.getName()); + UserTransaction tx1 = transactionService.getUserTransaction(); + tx1.begin(); + for (int i = 0; i < 20; i++) + { + HashSet refs = new HashSet(); + for (int j =0 ; j < i; j++) + { + ChildAssociationRef test = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}test"), testSuperType); + refs.add(test); + } + + for(ChildAssociationRef car : refs) + { + nodeService.deleteNode(car.getChildRef()); + } + + } + tx1.commit(); + System.out.println("End " + this.getName()); + } + catch (Exception e) + { + e.printStackTrace(); + System.exit(12); + } + finally + { + authenticationComponent.clearCurrentSecurityContext(); + } + if (waiter != null) + { + try + { + waiter.join(); + } + catch (InterruptedException e) + { + } + } + } + + } + + + + public void testDeltaIssue() throws Exception + { + final NodeService pns = (NodeService) ctx.getBean("NodeService"); + + testTX.commit(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + tx.commit(); + + Thread thread = new Thread(new Runnable() + { + + public void run() + { + try + { + UserTransaction tx = transactionService.getUserTransaction(); + tx = transactionService.getUserTransaction(); + tx.begin(); + + SearchParameters sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + ResultSet results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + Map props = new HashMap(); + props.put(ContentModel.PROP_TITLE, "woof"); + pns.addAspect(n1, ContentModel.ASPECT_TITLED, props); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + tx.rollback(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + + } + + }); + + thread.start(); + thread.join(); + + tx = transactionService.getUserTransaction(); + tx.begin(); + + SearchParameters sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + ResultSet results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + runBaseTests(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + Map props = new HashMap(); + props.put(ContentModel.PROP_TITLE, "woof"); + pns.addAspect(n1, ContentModel.ASPECT_TITLED, props); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + pns.setProperty(n1, ContentModel.PROP_TITLE, "cube"); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + tx.rollback(); + + } + + public void testRepeatPerformance() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + String query = "ID:\"" + rootNodeRef + "\""; + // check that we get the result + SearchParameters sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(query); + ResultSet results = searcher.query(sp); + assertEquals("No results found from query", 1, results.length()); + + long start = System.nanoTime(); + int count = 1000; + // repeat + for (int i = 0; i < count; i++) + { + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(query); + results = searcher.query(sp); + } + long end = System.nanoTime(); + // dump results + double duration = ((double) (end - start)) / 1E6; // duration in ms + double average = duration / (double) count; + System.out.println("Searched for identifier: \n" + + " count: " + count + "\n" + " average: " + average + " ms/search \n" + + " a million searches could take: " + (1E6 * average) / 1E3 / 60D + " minutes"); + // anything over 10ms is dire + if (average > 10.0) + { + logger.error("Search taking longer than 10ms: " + query); + } + } + + public void testSort() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + SearchParameters sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.addSort("ID", true); + ResultSet results = searcher.query(sp); + + String current = null; + for (ResultSetRow row : results) + { + String id = row.getNodeRef().getId(); + + if (current != null) + { + if (current.compareTo(id) > 0) + { + fail(); + } + } + current = id; + } + results.close(); + + SearchParameters sp2 = new SearchParameters(); + sp2.addStore(rootNodeRef.getStoreRef()); + sp2.setLanguage(SearchService.LANGUAGE_LUCENE); + sp2.setQuery("PATH:\"//.\""); + sp2.addSort("ID", false); + results = searcher.query(sp2); + + current = null; + for (ResultSetRow row : results) + { + String id = row.getNodeRef().getId(); + if (current != null) + { + if (current.compareTo(id) < 0) + { + fail(); + } + } + current = id; + } + results.close(); + + luceneFTS.resume(); + + + + SearchParameters sp3 = new SearchParameters(); + sp3.addStore(rootNodeRef.getStoreRef()); + sp3.setLanguage(SearchService.LANGUAGE_LUCENE); + sp3.setQuery("PATH:\"//.\""); + sp3.addSort(SearchParameters.SORT_IN_DOCUMENT_ORDER_ASCENDING); + results = searcher.query(sp3); + + int count = 0; + for (ResultSetRow row : results) + { + assertEquals(documentOrder[count++], row.getNodeRef()); + } + results.close(); + + SearchParameters sp4 = new SearchParameters(); + sp4.addStore(rootNodeRef.getStoreRef()); + sp4.setLanguage(SearchService.LANGUAGE_LUCENE); + sp4.setQuery("PATH:\"//.\""); + sp4.addSort(SearchParameters.SORT_IN_DOCUMENT_ORDER_DESCENDING); + results = searcher.query(sp4); + + count = 1; + for (ResultSetRow row : results) + { + assertEquals(documentOrder[documentOrder.length - (count++)], row.getNodeRef()); + } + results.close(); + + SearchParameters sp5 = new SearchParameters(); + sp5.addStore(rootNodeRef.getStoreRef()); + sp5.setLanguage(SearchService.LANGUAGE_LUCENE); + sp5.setQuery("PATH:\"//.\""); + sp5.addSort(SearchParameters.SORT_IN_SCORE_ORDER_ASCENDING); + results = searcher.query(sp5); + + float score = 0; + for (ResultSetRow row : results) + { + assertTrue(score <= row.getScore()); + score = row.getScore(); + } + results.close(); + + SearchParameters sp6 = new SearchParameters(); + sp6.addStore(rootNodeRef.getStoreRef()); + sp6.setLanguage(SearchService.LANGUAGE_LUCENE); + sp6.setQuery("PATH:\"//.\""); + sp6.addSort(SearchParameters.SORT_IN_SCORE_ORDER_DESCENDING); + results = searcher.query(sp6); + + score = 1.0f; + for (ResultSetRow row : results) + { + assertTrue(score >= row.getScore()); + score = row.getScore(); + } + results.close(); + + luceneFTS.resume(); + } + + public void test1() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + luceneFTS.resume(); + } + + public void test2() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + luceneFTS.resume(); + } + + public void test3() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + luceneFTS.resume(); + } + + public void test4() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setDictionaryService(dictionaryService); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "\\@\\{namespace\\}property\\-2:\"valuetwo\"", null, null); + results.close(); + luceneFTS.resume(); + } + + public void test5() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + luceneFTS.resume(); + } + + public void test6() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + luceneFTS.resume(); + } + + public void testNoOp() throws Exception + { + luceneFTS.pause(); + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis() + "_1", indexerAndSearcher); + + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + indexer.prepare(); + indexer.commit(); + luceneFTS.resume(); + } + + /** + * Test basic index and search + * + * @throws InterruptedException + * + */ + + public void testStandAloneIndexerCommit() throws Exception + { + luceneFTS.pause(); + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis() + "_1", indexerAndSearcher); + + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + // //indexer.clearIndex(); + + indexer.createNode(new ChildAssociationRef(null, null, null, rootNodeRef)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}one"), n1)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}two"), n2)); + indexer.updateNode(n1); + // indexer.deleteNode(new ChildRelationshipRef(rootNode, "path", + // newNode)); + + indexer.prepare(); + indexer.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "\\@\\{namespace\\}property\\-2:\"valuetwo\"", null, null); + simpleResultSetTest(results); + + ChildAssocRefResultSet r2 = new ChildAssocRefResultSet(nodeService, results.getNodeRefs(), null, false); + simpleResultSetTest(r2); + + ChildAssocRefResultSet r3 = new ChildAssocRefResultSet(nodeService, results.getNodeRefs(), null, true); + simpleResultSetTest(r3); + + ChildAssocRefResultSet r4 = new ChildAssocRefResultSet(nodeService, results.getChildAssocRefs(), null); + simpleResultSetTest(r4); + + DetachedResultSet r5 = new DetachedResultSet(results, null); + simpleResultSetTest(r5); + + DetachedResultSet r6 = new DetachedResultSet(r2, null); + simpleResultSetTest(r6); + + DetachedResultSet r7 = new DetachedResultSet(r3, null); + simpleResultSetTest(r7); + + DetachedResultSet r8 = new DetachedResultSet(r4, null); + simpleResultSetTest(r8); + + DetachedResultSet r9 = new DetachedResultSet(r5, null); + simpleResultSetTest(r9); + + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@\\{namespace\\}property\\-1:\"valueone\"", + null, null); + assertEquals(2, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n1.getId(), results.getNodeRef(1).getId()); + assertEquals(1.0f, results.getScore(0)); + assertEquals(1.0f, results.getScore(1)); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@namespace\\:property\\-1:\"valueone\"", null, + null); + assertEquals(2, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n1.getId(), results.getNodeRef(1).getId()); + assertEquals(1.0f, results.getScore(0)); + assertEquals(1.0f, results.getScore(1)); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@property\\-1:\"valueone\"", null, null); + assertEquals(2, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n1.getId(), results.getNodeRef(1).getId()); + assertEquals(1.0f, results.getScore(0)); + assertEquals(1.0f, results.getScore(1)); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@property\\-1:\"Valueone\"", null, null); + assertEquals(2, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n1.getId(), results.getNodeRef(1).getId()); + assertEquals(1.0f, results.getScore(0)); + assertEquals(1.0f, results.getScore(1)); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@property\\-1:ValueOne", null, null); + assertEquals(2, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n1.getId(), results.getNodeRef(1).getId()); + assertEquals(1.0f, results.getScore(0)); + assertEquals(1.0f, results.getScore(1)); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@property\\-1:valueone", null, null); + assertEquals(2, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n1.getId(), results.getNodeRef(1).getId()); + assertEquals(1.0f, results.getScore(0)); + assertEquals(1.0f, results.getScore(1)); + results.close(); + + QName qname = QName.createQName("", "property-1"); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "ID:\"" + n1.toString() + "\"", null, null); + + assertEquals(2, results.length()); + + results.close(); + luceneFTS.resume(); + } + + private void simpleResultSetTest(ResultSet results) + { + assertEquals(1, results.length()); + assertEquals(n2.getId(), results.getNodeRef(0).getId()); + assertEquals(n2, results.getNodeRef(0)); + assertEquals(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}two"), n2), results.getChildAssocRef(0)); + assertEquals(1, results.getChildAssocRefs().size()); + assertNotNull(results.getChildAssocRefs()); + assertEquals(0, results.getRow(0).getIndex()); + assertEquals(1.0f, results.getRow(0).getScore()); + assertEquals(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}two"), n2), results.getRow(0).getChildAssocRef()); + assertEquals(n2, results.getRow(0).getNodeRef()); + assertEquals(QName.createQName("{namespace}two"), results.getRow(0).getQName()); + assertEquals("valuetwo", results.getRow(0).getValue(QName.createQName("{namespace}property-2"))); + for (ResultSetRow row : results) + { + assertNotNull(row); + } + } + + public void testStandAlonePathIndexer() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "@\\{namespace\\}property-1:valueone", + null, null); + try + { + assertEquals(2, results.length()); + assertEquals(n1.getId(), results.getNodeRef(0).getId()); + assertEquals(n2.getId(), results.getNodeRef(1).getId()); + // assertEquals(1.0f, results.getScore(0)); + // assertEquals(1.0f, results.getScore(1)); + + QName qname = QName.createQName("", "property-1"); + + } finally + { + results.close(); + } + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "+ID:\"" + n1.toString() + "\"", null, null); + try + { + assertEquals(2, results.length()); + } finally + { + results.close(); + } + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "ID:\"" + rootNodeRef.toString() + "\"", null, + null); + try + { + assertEquals(1, results.length()); + } finally + { + results.close(); + } + luceneFTS.resume(); + } + + private void buildBaseIndex() + { + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis() + "_" + (new Random().nextInt()), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + // indexer.clearIndex(); + indexer.createNode(new ChildAssociationRef(null, null, null, rootNodeRef)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}one"), n1)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}two"), n2)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}three"), n3)); + indexer.createNode(new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, rootNodeRef, QName + .createQName("{namespace}four"), n4)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n1, QName.createQName("{namespace}five"), n5)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n1, QName.createQName("{namespace}six"), n6)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n2, QName.createQName("{namespace}seven"), n7)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n2, QName.createQName("{namespace}eight"), n8)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n5, QName.createQName("{namespace}nine"), n9)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n5, QName.createQName("{namespace}ten"), n10)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n5, QName.createQName("{namespace}eleven"), n11)); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n5, QName.createQName("{namespace}twelve"), n12)); + indexer + .createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n12, QName.createQName("{namespace}thirteen"), + n13)); + indexer + .createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n13, QName.createQName("{namespace}fourteen"), + n14)); + indexer.prepare(); + indexer.commit(); + } + + public void testAllPathSearch() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + + runBaseTests(); + luceneFTS.resume(); + } + + private void runBaseTests() + { + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setQueryRegister(queryRegisterComponent); + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one\"", null, null); + assertEquals(1, results.length()); + results.close(); + // results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + // "PATH:\"/\"", null, null); + // assertEquals(1, results.length()); + // results.close(); + // results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + // "PATH:\"/.\"", null, null); + // assertEquals(1, results.length()); + // results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:three\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:four\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:eight-0\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:five\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:six\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:seven\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-1\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-2\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-2\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-1\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:ten\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:eleven\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen/namespace:fourteen\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/namespace:*/namespace:*\"", + null, null); + assertEquals(8, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH_WITH_REPEATS:\"/namespace:*/namespace:*/namespace:*\"", null, null); + assertEquals(8, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*/namespace:*\"", + null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:*/namespace:five/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:*/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*\"", null, null); + assertEquals(8, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*/*\"", null, null); + assertEquals(8, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*/namespace:nine\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//.\"", null, null); + assertEquals(26, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//.\"", null, null); + assertEquals(15, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*\"", null, null); + assertEquals(25, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/.\"", null, null); + assertEquals(25, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/./.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/./.\"", null, null); + assertEquals(25, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//./*\"", null, null); + assertEquals(25, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//./*\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//././*/././.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//././*/././.\"", null, null); + assertEquals(25, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//common\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//common\"", null, null); + assertEquals(7, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//common\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one//common\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//*\"", null, null); + assertEquals(9, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//.\"", null, null); + assertEquals(7, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//.\"", null, null); + assertEquals(10, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//five/nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen//.\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen//.//.\"", null, + null); + assertEquals(1, results.length()); + results.close(); + + // Type search tests + + QName qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"1\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":1", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"01\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":01", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "@" + escapeQName(qname) + ":\"001\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@test\\:int\\-ista:\"0001\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":[0 TO 2]", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{0 TO 1}", null, + null); + assertEquals(0, results.length()); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{1 TO 2}", null, + null); + assertEquals(0, results.length()); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"2\"", null, null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"02\"", null, null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"002\"", null, null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"0002\"", null, null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":[0 TO 2]", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{0 TO 2}", null, + null); + assertEquals(0, results.length()); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "long-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{2 TO 3}", null, + null); + assertEquals(0, results.length()); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"3.4\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":[3 TO 4]", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":[3.3 TO 3.4]", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{3.3 TO 3.4}", null, + null); + assertEquals(0, results.length()); + results.close(); + + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"3.40\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"03.4\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "float-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":\"03.40\"", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "double-ista")) + ":\"5.6\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "double-ista")) + ":\"05.6\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "double-ista")) + ":\"5.60\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "double-ista")) + ":\"05.60\"", null, null); + assertEquals(1, results.length()); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "double-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":[5.5 TO 5.7]", null, + null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "double-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{5.5 TO 5.6}", null, + null); + assertEquals(0, results.length()); + results.close(); + + qname = QName.createQName(TEST_NAMESPACE, "double-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + escapeQName(qname) + ":{5.6 TO 5.7}", null, + null); + assertEquals(0, results.length()); + results.close(); + + + Date date = new Date(); + String sDate = CachingDateFormat.getDateFormat().format(date); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "date-ista")) + ":\"" + sDate + "\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "datetime-ista")) + ":\"" + sDate + "\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "boolean-ista")) + ":\"true\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "qname-ista")) + ":\"{wibble}wobble\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "category-ista")) + + ":\"" + + DefaultTypeConverter.INSTANCE.convert(String.class, new NodeRef(rootNodeRef.getStoreRef(), + "CategoryId")) + "\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "noderef-ista")) + ":\"" + n1 + "\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "path-ista")) + ":\"" + nodeService.getPath(n3) + "\"", + null, null); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(QName.createQName(TEST_NAMESPACE, "path-ista"))); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "TYPE:\"" + testType.toString() + "\"", null, + null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "TYPE:\"" + testSuperType.toString() + "\"", + null, null); + assertEquals(13, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "ASPECT:\"" + ISO9075.getXPathName(testAspect) + "\"", null, + null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "ASPECT:\"" + ISO9075.getXPathName(testSuperAspect) + "\"", + null, null); + assertEquals(1, results.length()); + results.close(); + + // FTS test + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "TEXT:\"fox\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "@"+LuceneQueryParser.escape(ContentModel.PROP_CONTENT.toString())+":\"fox\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "@"+LuceneQueryParser.escape(ContentModel.PROP_CONTENT.toString())+".mimetype:\"text/plain\"", null, null); + assertEquals(1, results.length()); + results.close(); + + QName queryQName = QName.createQName("alf:test1", namespacePrefixResolver); + results = searcher.query(rootNodeRef.getStoreRef(), queryQName, null); + assertEquals(1, results.length()); + results.close(); + + // Parameters + + queryQName = QName.createQName("alf:test2", namespacePrefixResolver); + results = searcher.query(rootNodeRef.getStoreRef(), queryQName, null); + assertEquals(1, results.length()); + results.close(); + + queryQName = QName.createQName("alf:test2", namespacePrefixResolver); + QueryParameter qp = new QueryParameter(QName.createQName("alf:banana", namespacePrefixResolver), "woof"); + results = searcher.query(rootNodeRef.getStoreRef(), queryQName, new QueryParameter[] { qp }); + assertEquals(0, results.length()); + results.close(); + + queryQName = QName.createQName("alf:test3", namespacePrefixResolver); + qp = new QueryParameter(QName.createQName("alf:banana", namespacePrefixResolver), "/one/five//*"); + results = searcher.query(rootNodeRef.getStoreRef(), queryQName, new QueryParameter[] { qp }); + assertEquals(6, results.length()); + results.close(); + + // TODO: should not have a null property type definition + QueryParameterDefImpl paramDef = new QueryParameterDefImpl(QName.createQName("alf:lemur", + namespacePrefixResolver), (DataTypeDefinition) null, true, "fox"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "TEXT:\"${alf:lemur}\"", null, + new QueryParameterDefinition[] { paramDef }); + assertEquals(1, results.length()); + results.close(); + + paramDef = new QueryParameterDefImpl(QName.createQName("alf:intvalue", namespacePrefixResolver), + (DataTypeDefinition) null, true, "1"); + qname = QName.createQName(TEST_NAMESPACE, "int-ista"); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(qname) + ":\"${alf:intvalue}\"", null, new QueryParameterDefinition[] { paramDef }); + assertEquals(1, results.length()); + assertNotNull(results.getRow(0).getValue(qname)); + results.close(); + + } + + public void testPathSearch() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + // //* + + ResultSet + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//common\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//common\"", null, null); + assertEquals(7, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//common\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one//common\"", null, null); + assertEquals(5, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//*\"", null, null); + assertEquals(9, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//.\"", null, null); + assertEquals(7, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//.\"", null, null); + assertEquals(10, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//five/nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen\"", null, null); + assertEquals(1, results.length()); + results.close(); + luceneFTS.resume(); + } + + public void testXPathSearch() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + // //* + + ResultSet + + results = searcher.query(rootNodeRef.getStoreRef(), "xpath", "//./*", null, null); + assertEquals(14, results.length()); + results.close(); + luceneFTS.resume(); + + QueryParameterDefinition paramDef = new QueryParameterDefImpl(QName.createQName("alf:query", + namespacePrefixResolver), (DataTypeDefinition) null, true, "//./*"); + results = searcher.query(rootNodeRef.getStoreRef(), "xpath", "${alf:query}", null, + new QueryParameterDefinition[] { paramDef }); + assertEquals(14, results.length()); + results.close(); + } + + public void testMissingIndex() throws Exception + { + luceneFTS.pause(); + StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "_missing_"); + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(storeRef, indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + // //* + + ResultSet + + results = searcher.query(storeRef, "xpath", "//./*", null, null); + assertEquals(0, results.length()); + luceneFTS.resume(); + } + + public void testUpdateIndex() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + + runBaseTests(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis(), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + indexer.updateNode(rootNodeRef); + indexer.updateNode(n1); + indexer.updateNode(n2); + indexer.updateNode(n3); + indexer.updateNode(n4); + indexer.updateNode(n5); + indexer.updateNode(n6); + indexer.updateNode(n7); + indexer.updateNode(n8); + indexer.updateNode(n9); + indexer.updateNode(n10); + indexer.updateNode(n11); + indexer.updateNode(n12); + indexer.updateNode(n13); + indexer.updateNode(n14); + + indexer.commit(); + + runBaseTests(); + luceneFTS.resume(); + } + + public void testDeleteLeaf() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis(), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + indexer + .deleteNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n13, QName.createQName("{namespace}fourteen"), + n14)); + + indexer.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:three\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:four\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:eight-0\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:five\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:six\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:seven\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-1\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-2\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-2\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-1\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:ten\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:eleven\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen/namespace:fourteen\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/namespace:*/namespace:*\"", + null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*/namespace:*\"", + null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:*\"", null, null); + assertEquals(3, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:*/namespace:five/namespace:*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:*/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*\"", null, null); + assertEquals(3, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*/namespace:nine\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//.\"", null, null); + assertEquals(17, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*\"", null, null); + assertEquals(13, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*\"", null, null); + assertEquals(16, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/.\"", null, null); + assertEquals(13, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/.\"", null, null); + assertEquals(16, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/./.\"", null, null); + assertEquals(13, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/./.\"", null, null); + assertEquals(16, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//./*\"", null, null); + assertEquals(13, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//./*\"", null, null); + assertEquals(16, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//././*/././.\"", null, null); + assertEquals(13, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//././*/././.\"", null, null); + assertEquals(16, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//common\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//common\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//.\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//five/nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen\"", null, null); + assertEquals(0, results.length()); + results.close(); + luceneFTS.resume(); + } + + public void testAddEscapedChild() throws Exception + { + String COMPLEX_LOCAL_NAME = " `¬¦!\"£$%^&*()-_=+\t\n\\\u0000[]{};'#:@~,./<>?\\|\u0123\u4567\u8900\uabcd\uefff_xT65A_"; + + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis(), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + ChildAssociationRef car = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}" + COMPLEX_LOCAL_NAME), testSuperType); + indexer.createNode(car); + + indexer.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:" + + ISO9075.encode(COMPLEX_LOCAL_NAME) + "\"", null, null); + assertEquals(1, results.length()); + results.close(); + } + + public void testDeleteContainer() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis(), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + indexer + .deleteNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, n12, QName.createQName("{namespace}thirteen"), + n13)); + + indexer.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:three\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:four\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:eight-0\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:five\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:six\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:seven\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-1\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-2\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-2\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-1\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:ten\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:eleven\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen/namespace:fourteen\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/namespace:*/namespace:*\"", + null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*/namespace:*\"", + null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:*\"", null, null); + assertEquals(3, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:*/namespace:five/namespace:*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:*/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*\"", null, null); + assertEquals(3, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*/namespace:nine\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//.\"", null, null); + assertEquals(13, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//.\"", null, null); + assertEquals(15, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*\"", null, null); + assertEquals(12, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/.\"", null, null); + assertEquals(12, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/./.\"", null, null); + assertEquals(12, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/./.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//./*\"", null, null); + assertEquals(12, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//./*\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//././*/././.\"", null, null); + assertEquals(12, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//././*/././.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//common\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//common\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//.\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//five/nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen\"", null, null); + assertEquals(0, results.length()); + results.close(); + luceneFTS.resume(); + } + + public void testDeleteAndAddReference() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis(), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + nodeService.removeChild(n2, n13); + indexer.deleteChildRelationship(new ChildAssociationRef(ASSOC_TYPE_QNAME, n2, QName + .createQName("{namespace}link"), n13)); + + indexer.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:three\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:four\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:eight-0\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:five\"", null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:one\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:two\"", null, + null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:six\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:seven\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-1\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-2\"", + null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-2\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-1\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:two/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:eight-0\"", + null, null); + assertEquals(0, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:ten\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:eleven\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:five/namespace:twelve/namespace:thirteen/namespace:fourteen\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/namespace:*/namespace:*\"", + null, null); + assertEquals(7, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:five\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:*/namespace:*/namespace:*\"", + null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH_WITH_REPEATS:\"/namespace:*/namespace:*/namespace:*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/namespace:*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:*/namespace:five/namespace:*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH:\"/namespace:one/namespace:*/namespace:nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*\"", null, null); + assertEquals(7, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/*/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/*/*/*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*\"", null, null); + assertEquals(4, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/*/namespace:five/*\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/namespace:one/*/namespace:nine\"", null, + null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//.\"", null, null); + assertEquals(15, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//.\"", null, null); + assertEquals(23, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*\"", null, null); + assertEquals(22, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/.\"", null, null); + assertEquals(22, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//*/./.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//*/./.\"", null, null); + assertEquals(22, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//./*\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//./*\"", null, null); + assertEquals(22, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//././*/././.\"", null, null); + assertEquals(14, results.length()); + results.close(); + results = searcher + .query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//././*/././.\"", null, null); + assertEquals(22, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//common\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//common\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//common\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one//common\"", null, null); + assertEquals(5, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//*\"", null, null); + assertEquals(6, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//*\"", null, null); + assertEquals(9, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one/five//.\"", null, null); + assertEquals(7, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"/one/five//.\"", null, null); + assertEquals(10, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//five/nine\"", null, null); + assertEquals(1, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"/one//thirteen/fourteen\"", null, null); + assertEquals(1, results.length()); + results.close(); + + indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + System.currentTimeMillis(), + indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + nodeService.addChild(n2, n13, ASSOC_TYPE_QNAME, QName.createQName("{namespace}link")); + indexer.createChildRelationship(new ChildAssociationRef(ASSOC_TYPE_QNAME, n2, QName + .createQName("{namespace}link"), n13)); + + indexer.commit(); + + runBaseTests(); + luceneFTS.resume(); + } + + public void testRenameReference() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//namespace:link//.\"", null, + null); + assertEquals(2, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH_WITH_REPEATS:\"//namespace:link//.\"", + null, null); + assertEquals(3, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//namespace:renamed_link//.\"", null, + null); + assertEquals(0, results.length()); + results.close(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis(), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + + nodeService.removeChild(n2, n13); + nodeService.addChild(n2, n13, ASSOC_TYPE_QNAME, QName.createQName("{namespace}renamed_link")); + + indexer.updateChildRelationship(new ChildAssociationRef(ASSOC_TYPE_QNAME, n2, QName.createQName("namespace", + "link"), n13), new ChildAssociationRef(ASSOC_TYPE_QNAME, n2, QName.createQName("namespace", + "renamed_link"), n13)); + + indexer.commit(); + + runBaseTests(); + + searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setDictionaryService(dictionaryService); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//namespace:link//.\"", null, null); + assertEquals(0, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PATH:\"//namespace:renamed_link//.\"", null, + null); + assertEquals(2, results.length()); + results.close(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", + "PATH_WITH_REPEATS:\"//namespace:renamed_link//.\"", null, null); + assertEquals(3, results.length()); + results.close(); + luceneFTS.resume(); + } + + public void testDelayIndex() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-atomic")) + + ":\"KEYONE\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-unstored-tokenised-atomic")) + + ":\"KEYUNSTORED\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-nonatomic")) + + ":\"KEYTWO\"", null, null); + assertEquals(0, results.length()); + results.close(); + + // Do index + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis() + "_" + (new Random().nextInt()), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + indexer.updateFullTextSearch(1000); + indexer.prepare(); + indexer.commit(); + + searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setDictionaryService(dictionaryService); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-atomic")) + + ":\"keyone\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-nonatomic")) + + ":\"keytwo\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-unstored-tokenised-atomic")) + + ":\"keyunstored\"", null, null); + assertEquals(1, results.length()); + results.close(); + + runBaseTests(); + luceneFTS.resume(); + } + + public void testWaitForIndex() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-atomic")) + + ":\"KEYONE\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-unstored-tokenised-atomic")) + + ":\"KEYUNSTORED\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-nonatomic")) + + ":\"KEYTWO\"", null, null); + assertEquals(0, results.length()); + results.close(); + + // Do index + + searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-atomic")) + + ":\"keyone\"", null, null); + assertEquals(1, results.length()); + results.close(); + + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis() + "_" + (new Random().nextInt()), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + indexer.updateFullTextSearch(1000); + indexer.prepare(); + indexer.commit(); + + luceneFTS.resume(); + // luceneFTS.requiresIndex(rootNodeRef.getStoreRef()); + // luceneFTS.index(); + // luceneFTS.index(); + // luceneFTS.index(); + + Thread.sleep(35000); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-stored-tokenised-nonatomic")) + + ":\"keytwo\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "\\@" + + escapeQName(QName.createQName(TEST_NAMESPACE, "text-indexed-unstored-tokenised-atomic")) + + ":\"KEYUNSTORED\"", null, null); + assertEquals(1, results.length()); + results.close(); + + runBaseTests(); + } + + private String escapeQName(QName qname) + { + return LuceneQueryParser.escape(qname.toString()); + } + + public void testForKev() throws Exception + { + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "PARENT:\"" + + rootNodeRef.toString() + "\"", null, null); + assertEquals(5, results.length()); + results.close(); + + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "+PARENT:\"" + + rootNodeRef.toString() + "\" +QNAME:\"one\"", null, null); + assertEquals(1, results.length()); + results.close(); + + results = searcher + .query( + rootNodeRef.getStoreRef(), + "lucene", + "( +TYPE:\"{http://www.alfresco.org/model/content/1.0}linkfile\" +@\\{http\\://www.alfresco.org/model/content/1.0\\}name:\"content woof\") OR TEXT:\"content\"", + null, null); + + luceneFTS.resume(); + } + + public void testIssueAR47() throws Exception + { + // This bug arose from repeated deletes and adds creating empty index + // segments. + // Two segements each containing one deletyed entry were merged together + // producing a single empty entry. + // This seemed to be bad for lucene - I am not sure why + + // So we add something, add and delete someting repeatedly and then + // check we can still do the search. + + // Running in autocommit against the index + testTX.commit(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + ChildAssociationRef testFind = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}testFind"), testSuperType); + tx.commit(); + + LuceneSearcherImpl searcher = LuceneSearcherImpl.getSearcher(rootNodeRef.getStoreRef(), indexerAndSearcher); + searcher.setNodeService(nodeService); + searcher.setDictionaryService(dictionaryService); + searcher.setNamespacePrefixResolver(getNamespacePrefixReolsver("namespace")); + searcher.setQueryRegister(queryRegisterComponent); + + ResultSet results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "QNAME:\"namespace:testFind\""); + assertEquals(1, results.length()); + results.close(); + + for (int i = 0; i < 100; i++) + { + UserTransaction tx1 = transactionService.getUserTransaction(); + tx1.begin(); + ChildAssociationRef test = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, QName + .createQName("{namespace}test"), testSuperType); + tx1.commit(); + + UserTransaction tx2 = transactionService.getUserTransaction(); + tx2.begin(); + nodeService.deleteNode(test.getChildRef()); + tx2.commit(); + } + + UserTransaction tx3 = transactionService.getUserTransaction(); + tx3.begin(); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "QNAME:\"namespace:testFind\""); + assertEquals(1, results.length()); + results.close(); + tx3.commit(); + } + + // Ignore the following test until implementation is completed + + public void testReadAgainstDelta() throws Exception + { + testTX.commit(); + UserTransaction tx = transactionService.getUserTransaction(); + tx.begin(); + luceneFTS.pause(); + buildBaseIndex(); + runBaseTests(); + tx.commit(); + + // Delete + + tx = transactionService.getUserTransaction(); + tx.begin(); + + runBaseTests(); + + serviceRegistry.getNodeService().deleteNode(n1); + + SearchParameters sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + ResultSet results = serviceRegistry.getSearchService().query(sp); + assertEquals(5, results.length()); + results.close(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(true); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + tx.rollback(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.addSort("ID", true); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + // Create + + tx = transactionService.getUserTransaction(); + tx.begin(); + + runBaseTests(); + + assertEquals(5, serviceRegistry.getNodeService().getChildAssocs(rootNodeRef).size()); + serviceRegistry.getNodeService().createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}texas"), testSuperType).getChildRef(); + assertEquals(6, serviceRegistry.getNodeService().getChildAssocs(rootNodeRef).size()); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(16, results.length()); + results.close(); + + tx.rollback(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.addSort("ID", true); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + // update property + + tx = transactionService.getUserTransaction(); + tx.begin(); + + runBaseTests(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("\\@\\{namespace\\}property\\-1:\"valueone\""); + sp.addSort("ID", true); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + + assertEquals(2, results.length()); + results.close(); + + nodeService.setProperty(n1, QName.createQName("{namespace}property-1"), "Different"); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("\\@\\{namespace\\}property\\-1:\"valueone\""); + sp.addSort("ID", true); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + + assertEquals(1, results.length()); + results.close(); + + tx.rollback(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("\\@\\{namespace\\}property\\-1:\"valueone\""); + sp.excludeDataInTheCurrentTransaction(false); + sp.addSort("ID", true); + results = serviceRegistry.getSearchService().query(sp); + + assertEquals(2, results.length()); + results.close(); + + // Add and delete + + tx = transactionService.getUserTransaction(); + tx.begin(); + + runBaseTests(); + + serviceRegistry.getNodeService().deleteNode(n1); + serviceRegistry.getNodeService().createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}texas"), testSuperType).getChildRef(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(6, results.length()); + results.close(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.excludeDataInTheCurrentTransaction(true); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + tx.rollback(); + + sp = new SearchParameters(); + sp.addStore(rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"//.\""); + sp.addSort("ID", true); + sp.excludeDataInTheCurrentTransaction(false); + results = serviceRegistry.getSearchService().query(sp); + assertEquals(15, results.length()); + results.close(); + + } + + private void runPerformanceTest(double time, boolean clear) + { + LuceneIndexerImpl indexer = LuceneIndexerImpl.getUpdateIndexer(rootNodeRef.getStoreRef(), "delta" + + System.currentTimeMillis() + "_" + (new Random().nextInt()), indexerAndSearcher); + indexer.setNodeService(nodeService); + indexer.setLuceneIndexLock(luceneIndexLock); + indexer.setDictionaryService(dictionaryService); + indexer.setLuceneFullTextSearchIndexer(luceneFTS); + indexer.setContentService(contentService); + if (clear) + { + // indexer.clearIndex(); + } + indexer.createNode(new ChildAssociationRef(null, null, null, rootNodeRef)); + + long startTime = System.currentTimeMillis(); + int count = 0; + for (int i = 0; i < 10000000; i++) + { + if (i % 10 == 0) + { + if (System.currentTimeMillis() - startTime > time) + { + count = i; + break; + } + } + + QName qname = QName.createQName("{namespace}a_" + i); + NodeRef ref = nodeService.createNode(rootNodeRef, ASSOC_TYPE_QNAME, qname, ContentModel.TYPE_CONTAINER) + .getChildRef(); + indexer.createNode(new ChildAssociationRef(ASSOC_TYPE_QNAME, rootNodeRef, qname, ref)); + + } + indexer.commit(); + float delta = ((System.currentTimeMillis() - startTime) / 1000.0f); + // System.out.println("\tCreated " + count + " in " + delta + " = " + + // (count / delta)); + } + + private NamespacePrefixResolver getNamespacePrefixReolsver(String defaultURI) + { + DynamicNamespacePrefixResolver nspr = new DynamicNamespacePrefixResolver(null); + nspr.registerNamespace(NamespaceService.ALFRESCO_PREFIX, NamespaceService.ALFRESCO_URI); + nspr.registerNamespace(NamespaceService.CONTENT_MODEL_PREFIX, NamespaceService.CONTENT_MODEL_1_0_URI); + nspr.registerNamespace("namespace", "namespace"); + nspr.registerNamespace("test", TEST_NAMESPACE); + nspr.registerNamespace(NamespaceService.DEFAULT_PREFIX, defaultURI); + return nspr; + } + + public static void main(String[] args) throws Exception + { + LuceneTest test = new LuceneTest(); + test.setUp(); + // test.testForKev(); + // test.testDeleteContainer(); + + // test.testReadAgainstDelta(); + + NodeRef targetNode = test.rootNodeRef; + Path path = test.serviceRegistry.getNodeService().getPath(targetNode); + + SearchParameters sp = new SearchParameters(); + sp.addStore(test.rootNodeRef.getStoreRef()); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("PATH:\"" + path + "//." + "\""); + ResultSet results = test.serviceRegistry.getSearchService().query(sp); + + results.close(); + + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneTest_model.xml b/source/java/org/alfresco/repo/search/impl/lucene/LuceneTest_model.xml new file mode 100644 index 0000000000..5f2a0dcd64 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneTest_model.xml @@ -0,0 +1,187 @@ + + + Test Model for Lucene tests + Alfresco + 2005-07-13 + 0.1 + + + + + + + + + + + + + Test Super Type + sys:container + + + + false + true + + + sys:base + false + true + + + + + + + Test Type + test:testSuperType + + + d:text + true + false + + true + true + true + + + + d:text + true + false + + true + false + true + + + + d:text + true + false + + false + true + true + + + + d:int + true + false + + true + true + true + + + + d:long + true + false + + true + true + true + + + + d:float + true + false + + true + true + true + + + + d:double + true + false + + true + true + true + + + + d:date + true + false + + true + true + true + + + + d:datetime + true + false + + true + true + true + + + + d:boolean + true + false + + true + true + true + + + + d:qname + true + false + + true + true + true + + + + d:category + true + false + + true + true + true + + + + d:noderef + true + false + + true + true + true + + + + + test:testAspect + + + + + + + Test Super Aspect + + + Titled + test:testSuperAspect + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneXPathHandler.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneXPathHandler.java new file mode 100644 index 0000000000..db9abc4c9b --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneXPathHandler.java @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import java.util.ArrayList; + +import org.alfresco.repo.search.impl.lucene.analysis.PathTokenFilter; +import org.alfresco.repo.search.impl.lucene.query.AbsoluteStructuredFieldPosition; +import org.alfresco.repo.search.impl.lucene.query.DescendantAndSelfStructuredFieldPosition; +import org.alfresco.repo.search.impl.lucene.query.PathQuery; +import org.alfresco.repo.search.impl.lucene.query.RelativeStructuredFieldPosition; +import org.alfresco.repo.search.impl.lucene.query.SelfAxisStructuredFieldPosition; +import org.alfresco.repo.search.impl.lucene.query.StructuredFieldPosition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.saxpath.Axis; +import org.saxpath.Operator; +import org.saxpath.SAXPathException; +import org.saxpath.XPathHandler; + +public class LuceneXPathHandler implements XPathHandler +{ + private PathQuery query; + + private boolean isAbsolutePath = true; + + int absolutePosition = 0; + + private NamespacePrefixResolver namespacePrefixResolver; + + private DictionaryService dictionaryService; + + public LuceneXPathHandler() + { + super(); + } + + public PathQuery getQuery() + { + return this.query; + } + + public void endAbsoluteLocationPath() throws SAXPathException + { + // No action + } + + public void endAdditiveExpr(int op) throws SAXPathException + { + switch (op) + { + case Operator.NO_OP: + break; + case Operator.ADD: + case Operator.SUBTRACT: + throw new UnsupportedOperationException(); + default: + throw new UnsupportedOperationException("Unknown operation " + op); + } + } + + public void endAllNodeStep() throws SAXPathException + { + // Nothing to do + // Todo: Predicates + } + + public void endAndExpr(boolean create) throws SAXPathException + { + if (create) + { + throw new UnsupportedOperationException(); + } + } + + public void endCommentNodeStep() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void endEqualityExpr(int op) throws SAXPathException + { + switch (op) + { + case Operator.NO_OP: + break; + case Operator.EQUALS: + case Operator.NOT_EQUALS: + throw new UnsupportedOperationException(); + default: + throw new UnsupportedOperationException("Unknown operation " + op); + } + } + + public void endFilterExpr() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void endFunction() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void endMultiplicativeExpr(int op) throws SAXPathException + { + switch (op) + { + case Operator.NO_OP: + break; + case Operator.MULTIPLY: + case Operator.DIV: + case Operator.MOD: + throw new UnsupportedOperationException(); + default: + throw new UnsupportedOperationException("Unknown operation " + op); + } + } + + public void endNameStep() throws SAXPathException + { + // Do nothing at the moment + // Could have repdicates + } + + public void endOrExpr(boolean create) throws SAXPathException + { + if (create) + { + throw new UnsupportedOperationException(); + } + } + + public void endPathExpr() throws SAXPathException + { + // Already built + } + + public void endPredicate() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void endProcessingInstructionNodeStep() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void endRelationalExpr(int op) throws SAXPathException + { + switch (op) + { + case Operator.NO_OP: + break; + case Operator.GREATER_THAN: + case Operator.GREATER_THAN_EQUALS: + case Operator.LESS_THAN: + case Operator.LESS_THAN_EQUALS: + throw new UnsupportedOperationException(); + default: + throw new UnsupportedOperationException("Unknown operation " + op); + } + } + + public void endRelativeLocationPath() throws SAXPathException + { + // No action + } + + public void endTextNodeStep() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void endUnaryExpr(int op) throws SAXPathException + { + switch (op) + { + case Operator.NO_OP: + break; + case Operator.NEGATIVE: + throw new UnsupportedOperationException(); + default: + throw new UnsupportedOperationException("Unknown operation " + op); + } + } + + public void endUnionExpr(boolean create) throws SAXPathException + { + if (create) + { + throw new UnsupportedOperationException(); + } + } + + public void endXPath() throws SAXPathException + { + // Do nothing at the moment + } + + public void literal(String arg0) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void number(double arg0) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void number(int arg0) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startAbsoluteLocationPath() throws SAXPathException + { + if (!isAbsolutePath) + { + throw new IllegalStateException(); + } + + } + + public void startAdditiveExpr() throws SAXPathException + { + // Do nothing at the moment + } + + public void startAllNodeStep(int axis) throws SAXPathException + { + switch (axis) + { + case Axis.CHILD: + if (isAbsolutePath) + { + // addAbsolute(null, null); + // We can always do relative stuff + addRelative(null, null); + } + else + { + addRelative(null, null); + } + break; + case Axis.DESCENDANT_OR_SELF: + query.appendQuery(getArrayList(new DescendantAndSelfStructuredFieldPosition(), new DescendantAndSelfStructuredFieldPosition())); + break; + case Axis.SELF: + query.appendQuery(getArrayList(new SelfAxisStructuredFieldPosition(), new SelfAxisStructuredFieldPosition())); + break; + default: + throw new UnsupportedOperationException(); + } + } + + private ArrayList getArrayList(StructuredFieldPosition one, StructuredFieldPosition two) + { + ArrayList answer = new ArrayList(2); + answer.add(one); + answer.add(two); + return answer; + } + + public void startAndExpr() throws SAXPathException + { + // Do nothing + } + + public void startCommentNodeStep(int arg0) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startEqualityExpr() throws SAXPathException + { + // Do nothing + } + + public void startFilterExpr() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startFunction(String arg0, String arg1) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startMultiplicativeExpr() throws SAXPathException + { + // Do nothing at the moment + } + + public void startNameStep(int axis, String nameSpace, String localName) throws SAXPathException + { + switch (axis) + { + case Axis.CHILD: + if (isAbsolutePath) + { + // addAbsolute(nameSpace, localName); + // we can always do relative stuff + addRelative(nameSpace, localName); + } + else + { + addRelative(nameSpace, localName); + } + break; + default: + throw new UnsupportedOperationException(); + } + + } + + private void addAbsolute(String nameSpace, String localName) + { + ArrayList answer = new ArrayList(2); + // TODO: Resolve name space + absolutePosition++; + if ((nameSpace == null) || (nameSpace.length() == 0)) + { + + if(localName.equals("*")) + { + answer.add(new RelativeStructuredFieldPosition("*")); + } + else if (namespacePrefixResolver.getNamespaceURI("") == null) + { + answer.add(new AbsoluteStructuredFieldPosition(PathTokenFilter.NO_NS_TOKEN_TEXT, absolutePosition)); + } + else + { + answer.add(new AbsoluteStructuredFieldPosition(namespacePrefixResolver.getNamespaceURI(""), absolutePosition)); + } + + } + else + { + answer.add(new AbsoluteStructuredFieldPosition(namespacePrefixResolver.getNamespaceURI(nameSpace), absolutePosition)); + } + + absolutePosition++; + if ((localName == null) || (localName.length() == 0)) + { + answer.add(new AbsoluteStructuredFieldPosition("*", absolutePosition)); + } + else + { + answer.add(new AbsoluteStructuredFieldPosition(localName, absolutePosition)); + } + query.appendQuery(answer); + + } + + private void addRelative(String nameSpace, String localName) + { + ArrayList answer = new ArrayList(2); + if ((nameSpace == null) || (nameSpace.length() == 0)) + { + if(localName.equals("*")) + { + answer.add(new RelativeStructuredFieldPosition("*")); + } + else if (namespacePrefixResolver.getNamespaceURI("") == null) + { + answer.add(new RelativeStructuredFieldPosition(PathTokenFilter.NO_NS_TOKEN_TEXT)); + } + else + { + answer.add(new RelativeStructuredFieldPosition(namespacePrefixResolver.getNamespaceURI(""))); + } + } + else + { + answer.add(new RelativeStructuredFieldPosition(namespacePrefixResolver.getNamespaceURI(nameSpace))); + } + + if ((localName == null) || (localName.length() == 0)) + { + answer.add(new RelativeStructuredFieldPosition("*")); + } + else + { + answer.add(new RelativeStructuredFieldPosition(localName)); + } + query.appendQuery(answer); + } + + public void startOrExpr() throws SAXPathException + { + // Do nothing at the moment + } + + public void startPathExpr() throws SAXPathException + { + // Just need one! + } + + public void startPredicate() throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startProcessingInstructionNodeStep(int arg0, String arg1) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startRelationalExpr() throws SAXPathException + { + // Do nothing at the moment + } + + public void startRelativeLocationPath() throws SAXPathException + { + isAbsolutePath = false; + } + + public void startTextNodeStep(int arg0) throws SAXPathException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void startUnaryExpr() throws SAXPathException + { + // Do nothing for now + } + + public void startUnionExpr() throws SAXPathException + { + // Do nothing at the moment + } + + public void startXPath() throws SAXPathException + { + query = new PathQuery(dictionaryService); + } + + public void variableReference(String uri, String localName) throws SAXPathException + { + + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + + + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/ParseException.java b/source/java/org/alfresco/repo/search/impl/lucene/ParseException.java new file mode 100644 index 0000000000..c19638b39f --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/ParseException.java @@ -0,0 +1,192 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +package org.alfresco.repo.search.impl.lucene; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + String expected = ""; + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected += tokenImage[expectedTokenSequences[i][j]] + " "; + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected += "..."; + } + expected += eol + " "; + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected; + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/QueryParameterisationException.java b/source/java/org/alfresco/repo/search/impl/lucene/QueryParameterisationException.java new file mode 100644 index 0000000000..d9e83761c9 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/QueryParameterisationException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class QueryParameterisationException extends AlfrescoRuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = 1L; + + public QueryParameterisationException(String msg) + { + super(msg); + } + + public QueryParameterisationException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/QueryParser.java b/source/java/org/alfresco/repo/search/impl/lucene/QueryParser.java new file mode 100644 index 0000000000..d5510eeb94 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/QueryParser.java @@ -0,0 +1,1206 @@ +/* Generated By:JavaCC: Do not edit this line. QueryParser.java */ +package org.alfresco.repo.search.impl.lucene; + +import java.io.IOException; +import java.io.StringReader; +import java.text.DateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Vector; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.document.DateField; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.RangeQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.WildcardQuery; + +/** + * This class is generated by JavaCC. The only method that clients should need + * to call is parse(). + * + * The syntax for query strings is as follows: + * A Query is a series of clauses. + * A clause may be prefixed by: + *
      + *
    • a plus (+) or a minus (-) sign, indicating + * that the clause is required or prohibited respectively; or + *
    • a term followed by a colon, indicating the field to be searched. + * This enables one to construct queries which search multiple fields. + *
    + * + * A clause may be either: + *
      + *
    • a term, indicating all the documents that contain this term; or + *
    • a nested query, enclosed in parentheses. Note that this may be used + * with a +/- prefix to require any of a set of + * terms. + *
    + * + * Thus, in BNF, the query grammar is: + *
    + *   Query  ::= ( Clause )*
    + *   Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")" )
    + * 
    + * + *

    + * Examples of appropriately formatted queries can be found in the test cases. + *

    + * + * @author Brian Goetz + * @author Peter Halacsy + * @author Tatu Saloranta + */ + +public class QueryParser implements QueryParserConstants { + + private static final int CONJ_NONE = 0; + private static final int CONJ_AND = 1; + private static final int CONJ_OR = 2; + + private static final int MOD_NONE = 0; + private static final int MOD_NOT = 10; + private static final int MOD_REQ = 11; + + public static final int DEFAULT_OPERATOR_OR = 0; + public static final int DEFAULT_OPERATOR_AND = 1; + + /** The actual operator that parser uses to combine query terms */ + private int operator = DEFAULT_OPERATOR_OR; + + /** + * Whether terms of wildcard and prefix queries are to be automatically + * lower-cased or not. Default is true. + */ + boolean lowercaseWildcardTerms = true; + + Analyzer analyzer; + String field; + int phraseSlop = 0; + float fuzzyMinSim = FuzzyQuery.defaultMinSimilarity; + Locale locale = Locale.getDefault(); + + /** Parses a query string, returning a {@link org.apache.lucene.search.Query}. + * @param query the query string to be parsed. + * @param field the default field for query terms. + * @param analyzer used to find terms in the query text. + * @throws ParseException if the parsing fails + */ + static public Query parse(String query, String field, Analyzer analyzer) + throws ParseException { + QueryParser parser = new QueryParser(field, analyzer); + return parser.parse(query); + } + + /** Constructs a query parser. + * @param f the default field for query terms. + * @param a used to find terms in the query text. + */ + public QueryParser(String f, Analyzer a) { + this(new FastCharStream(new StringReader(""))); + analyzer = a; + field = f; + } + + /** Parses a query string, returning a + * Query. + * @param query the query string to be parsed. + * @throws ParseException if the parsing fails + */ + public Query parse(String query) throws ParseException { + ReInit(new FastCharStream(new StringReader(query))); + try { + return Query(field); + } + catch (TokenMgrError tme) { + throw new ParseException(tme.getMessage()); + } + catch (BooleanQuery.TooManyClauses tmc) { + throw new ParseException("Too many boolean clauses"); + } + } + + /** + * @return Returns the analyzer. + */ + public Analyzer getAnalyzer() { + return analyzer; + } + + /** + * @return Returns the field. + */ + public String getField() { + return field; + } + + /** + * Get the default minimal similarity for fuzzy queries. + */ + public float getFuzzyMinSim() { + return fuzzyMinSim; + } + /** + *Set the default minimum similarity for fuzzy queries. + */ + public void setFuzzyMinSim(float fuzzyMinSim) { + this.fuzzyMinSim = fuzzyMinSim; + } + + /** + * Sets the default slop for phrases. If zero, then exact phrase matches + * are required. Default value is zero. + */ + public void setPhraseSlop(int phraseSlop) { + this.phraseSlop = phraseSlop; + } + + /** + * Gets the default slop for phrases. + */ + public int getPhraseSlop() { + return phraseSlop; + } + + /** + * Sets the boolean operator of the QueryParser. + * In classic mode (DEFAULT_OPERATOR_OR) terms without any modifiers + * are considered optional: for example capital of Hungary is equal to + * capital OR of OR Hungary.
    + * In DEFAULT_OPERATOR_AND terms are considered to be in conjuction: the + * above mentioned query is parsed as capital AND of AND Hungary + */ + public void setOperator(int operator) { + this.operator = operator; + } + + /** + * Gets implicit operator setting, which will be either DEFAULT_OPERATOR_AND + * or DEFAULT_OPERATOR_OR. + */ + public int getOperator() { + return operator; + } + + public void setLowercaseWildcardTerms(boolean lowercaseWildcardTerms) { + this.lowercaseWildcardTerms = lowercaseWildcardTerms; + } + + public boolean getLowercaseWildcardTerms() { + return lowercaseWildcardTerms; + } + + /** + * Set locale used by date range parsing. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Returns current locale, allowing access by subclasses. + */ + public Locale getLocale() { + return locale; + } + + protected void addClause(Vector clauses, int conj, int mods, Query q) { + boolean required, prohibited; + + // If this term is introduced by AND, make the preceding term required, + // unless it's already prohibited + if (clauses.size() > 0 && conj == CONJ_AND) { + BooleanClause c = (BooleanClause) clauses.elementAt(clauses.size()-1); + if (!c.prohibited) + c.required = true; + } + + if (clauses.size() > 0 && operator == DEFAULT_OPERATOR_AND && conj == CONJ_OR) { + // If this term is introduced by OR, make the preceding term optional, + // unless it's prohibited (that means we leave -a OR b but +a OR b-->a OR b) + // notice if the input is a OR b, first term is parsed as required; without + // this modification a OR b would parsed as +a OR b + BooleanClause c = (BooleanClause) clauses.elementAt(clauses.size()-1); + if (!c.prohibited) + c.required = false; + } + + // We might have been passed a null query; the term might have been + // filtered away by the analyzer. + if (q == null) + return; + + if (operator == DEFAULT_OPERATOR_OR) { + // We set REQUIRED if we're introduced by AND or +; PROHIBITED if + // introduced by NOT or -; make sure not to set both. + prohibited = (mods == MOD_NOT); + required = (mods == MOD_REQ); + if (conj == CONJ_AND && !prohibited) { + required = true; + } + } else { + // We set PROHIBITED if we're introduced by NOT or -; We set REQUIRED + // if not PROHIBITED and not introduced by OR + prohibited = (mods == MOD_NOT); + required = (!prohibited && conj != CONJ_OR); + } + clauses.addElement(new BooleanClause(q, required, prohibited)); + } + + /** + * Note that parameter analyzer is ignored. Calls inside the parser always + * use class member analyser. This method will be deprecated and substituted + * by {@link #getFieldQuery(String, String)} in future versions of Lucene. + * Currently overwriting either of these methods works. + * + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, + Analyzer analyzer, + String queryText) throws ParseException { + return getFieldQuery(field, queryText); + } + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText) throws ParseException { + // Use the analyzer to get all the tokens, and then build a TermQuery, + // PhraseQuery, or nothing based on the term count + + TokenStream source = analyzer.tokenStream(field, + new StringReader(queryText)); + Vector v = new Vector(); + org.apache.lucene.analysis.Token t; + + while (true) { + try { + t = source.next(); + } + catch (IOException e) { + t = null; + } + if (t == null) + break; + v.addElement(t.termText()); + } + try { + source.close(); + } + catch (IOException e) { + // ignore + } + + if (v.size() == 0) + return null; + else if (v.size() == 1) + return new TermQuery(new Term(field, (String) v.elementAt(0))); + else { + PhraseQuery q = new PhraseQuery(); + q.setSlop(phraseSlop); + for (int i=0; i + * Depending on settings, prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

    + * Can be overridden by extending classes, to provide custom handling for + * wildcard queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token that contains one or more wild card + * characters (? or *), but is not simple prefix term + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getWildcardQuery(String field, String termStr) throws ParseException + { + if (lowercaseWildcardTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return new WildcardQuery(t); + } + + /** + * Factory method for generating a query (similar to + * ({@link #getWildcardQuery}). Called when parser parses an input term + * token that uses prefix notation; that is, contains a single '*' wildcard + * character as its last character. Since this is a special case + * of generic wildcard term, and such a query can be optimized easily, + * this usually results in a different query object. + *

    + * Depending on settings, a prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

    + * Can be overridden by extending classes, to provide custom handling for + * wild card queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * (without trailing '*' character!) + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getPrefixQuery(String field, String termStr) throws ParseException + { + if (lowercaseWildcardTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return new PrefixQuery(t); + } + + /** + * Factory method for generating a query (similar to + * ({@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr) throws ParseException { + return getFuzzyQuery(field, termStr, fuzzyMinSim); + } + + /** + * Factory method for generating a query (similar to + * ({@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~floatNumber) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * @param minSimilarity the minimum similarity required for a fuzzy match + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException + { + Term t = new Term(field, termStr); + return new FuzzyQuery(t, minSimilarity); + } + + /** + * Returns a String where the escape char has been + * removed, or kept only once if there was a double escape. + */ + private String discardEscapeChar(String input) { + char[] caSource = input.toCharArray(); + char[] caDest = new char[caSource.length]; + int j = 0; + for (int i = 0; i < caSource.length; i++) { + if ((caSource[i] != '\\') || (i > 0 && caSource[i-1] == '\\')) { + caDest[j++]=caSource[i]; + } + } + return new String(caDest, 0, j); + } + + /** + * Returns a String where those characters that QueryParser + * expects to be escaped are escaped, i.e. preceded by a \. + */ + public static String escape(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // NOTE: keep this in sync with _ESCAPED_CHAR below! + if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' + || c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~' + || c == '*' || c == '?') { + sb.append('\\'); + } + sb.append(c); + } + return sb.toString(); + } + + public static void main(String[] args) throws Exception { + QueryParser qp = new QueryParser("field", + new org.apache.lucene.analysis.SimpleAnalyzer()); + Query q = qp.parse(args[0]); + System.out.println(q.toString("field")); + } + +// * Query ::= ( Clause )* +// * Clause ::= ["+", "-"] [ ":"] ( | "(" Query ")" ) + final public int Conjunction() throws ParseException { + int ret = CONJ_NONE; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + case OR: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + jj_consume_token(AND); + ret = CONJ_AND; + break; + case OR: + jj_consume_token(OR); + ret = CONJ_OR; + break; + default: + jj_la1[0] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[1] = jj_gen; + ; + } + {if (true) return ret;} + throw new Error("Missing return statement in function"); + } + + final public int Modifiers() throws ParseException { + int ret = MOD_NONE; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + case PLUS: + case MINUS: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PLUS: + jj_consume_token(PLUS); + ret = MOD_REQ; + break; + case MINUS: + jj_consume_token(MINUS); + ret = MOD_NOT; + break; + case NOT: + jj_consume_token(NOT); + ret = MOD_NOT; + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[3] = jj_gen; + ; + } + {if (true) return ret;} + throw new Error("Missing return statement in function"); + } + + final public Query Query(String field) throws ParseException { + Vector clauses = new Vector(); + Query q, firstQuery=null; + int conj, mods; + mods = Modifiers(); + q = Clause(field); + addClause(clauses, CONJ_NONE, mods, q); + if (mods == MOD_NONE) + firstQuery=q; + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + case OR: + case NOT: + case PLUS: + case MINUS: + case LPAREN: + case QUOTED: + case TERM: + case PREFIXTERM: + case WILDTERM: + case RANGEIN_START: + case RANGEEX_START: + case NUMBER: + ; + break; + default: + jj_la1[4] = jj_gen; + break label_1; + } + conj = Conjunction(); + mods = Modifiers(); + q = Clause(field); + addClause(clauses, conj, mods, q); + } + if (clauses.size() == 1 && firstQuery != null) + {if (true) return firstQuery;} + else { + {if (true) return getBooleanQuery(clauses);} + } + throw new Error("Missing return statement in function"); + } + + final public Query Clause(String field) throws ParseException { + Query q; + Token fieldToken=null, boost=null; + if (jj_2_1(2)) { + fieldToken = jj_consume_token(TERM); + jj_consume_token(COLON); + field=discardEscapeChar(fieldToken.image); + } else { + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QUOTED: + case TERM: + case PREFIXTERM: + case WILDTERM: + case RANGEIN_START: + case RANGEEX_START: + case NUMBER: + q = Term(field); + break; + case LPAREN: + jj_consume_token(LPAREN); + q = Query(field); + jj_consume_token(RPAREN); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[5] = jj_gen; + ; + } + break; + default: + jj_la1[6] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + if (boost != null) { + float f = (float)1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + q.setBoost(f); + } catch (Exception ignored) { } + } + {if (true) return q;} + throw new Error("Missing return statement in function"); + } + + final public Query Term(String field) throws ParseException { + Token term, boost=null, fuzzySlop=null, goop1, goop2; + boolean prefix = false; + boolean wildcard = false; + boolean fuzzy = false; + boolean rangein = false; + Query q; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TERM: + case PREFIXTERM: + case WILDTERM: + case NUMBER: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TERM: + term = jj_consume_token(TERM); + break; + case PREFIXTERM: + term = jj_consume_token(PREFIXTERM); + prefix=true; + break; + case WILDTERM: + term = jj_consume_token(WILDTERM); + wildcard=true; + break; + case NUMBER: + term = jj_consume_token(NUMBER); + break; + default: + jj_la1[7] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + fuzzy=true; + break; + default: + jj_la1[8] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + fuzzy=true; + break; + default: + jj_la1[9] = jj_gen; + ; + } + break; + default: + jj_la1[10] = jj_gen; + ; + } + String termImage=discardEscapeChar(term.image); + if (wildcard) { + q = getWildcardQuery(field, termImage); + } else if (prefix) { + q = getPrefixQuery(field, + discardEscapeChar(term.image.substring + (0, term.image.length()-1))); + } else if (fuzzy) { + float fms = fuzzyMinSim; + try { + fms = Float.valueOf(fuzzySlop.image.substring(1)).floatValue(); + } catch (Exception ignored) { } + if(fms < 0.0f || fms > 1.0f){ + {if (true) throw new ParseException("Minimum similarity for a FuzzyQuery has to be between 0.0f and 1.0f !");} + } + if(fms == fuzzyMinSim) + q = getFuzzyQuery(field, termImage); + else + q = getFuzzyQuery(field, termImage, fms); + } else { + q = getFieldQuery(field, analyzer, termImage); + } + break; + case RANGEIN_START: + jj_consume_token(RANGEIN_START); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEIN_GOOP: + goop1 = jj_consume_token(RANGEIN_GOOP); + break; + case RANGEIN_QUOTED: + goop1 = jj_consume_token(RANGEIN_QUOTED); + break; + default: + jj_la1[11] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEIN_TO: + jj_consume_token(RANGEIN_TO); + break; + default: + jj_la1[12] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEIN_GOOP: + goop2 = jj_consume_token(RANGEIN_GOOP); + break; + case RANGEIN_QUOTED: + goop2 = jj_consume_token(RANGEIN_QUOTED); + break; + default: + jj_la1[13] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RANGEIN_END); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[14] = jj_gen; + ; + } + if (goop1.kind == RANGEIN_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } else { + goop1.image = discardEscapeChar(goop1.image); + } + if (goop2.kind == RANGEIN_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } else { + goop2.image = discardEscapeChar(goop2.image); + } + q = getRangeQuery(field, analyzer, goop1.image, goop2.image, true); + break; + case RANGEEX_START: + jj_consume_token(RANGEEX_START); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEEX_GOOP: + goop1 = jj_consume_token(RANGEEX_GOOP); + break; + case RANGEEX_QUOTED: + goop1 = jj_consume_token(RANGEEX_QUOTED); + break; + default: + jj_la1[15] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEEX_TO: + jj_consume_token(RANGEEX_TO); + break; + default: + jj_la1[16] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEEX_GOOP: + goop2 = jj_consume_token(RANGEEX_GOOP); + break; + case RANGEEX_QUOTED: + goop2 = jj_consume_token(RANGEEX_QUOTED); + break; + default: + jj_la1[17] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RANGEEX_END); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[18] = jj_gen; + ; + } + if (goop1.kind == RANGEEX_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } else { + goop1.image = discardEscapeChar(goop1.image); + } + if (goop2.kind == RANGEEX_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } else { + goop2.image = discardEscapeChar(goop2.image); + } + + q = getRangeQuery(field, analyzer, goop1.image, goop2.image, false); + break; + case QUOTED: + term = jj_consume_token(QUOTED); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + break; + default: + jj_la1[19] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[20] = jj_gen; + ; + } + int s = phraseSlop; + + if (fuzzySlop != null) { + try { + s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { } + } + q = getFieldQuery(field, analyzer, term.image.substring(1, term.image.length()-1), s); + break; + default: + jj_la1[21] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + if (boost != null) { + float f = (float) 1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + } + catch (Exception ignored) { + /* Should this be handled somehow? (defaults to "no boost", if + * boost number is invalid) + */ + } + + // avoid boosting null queries, such as those caused by stop words + if (q != null) { + q.setBoost(f); + } + } + {if (true) return q;} + throw new Error("Missing return statement in function"); + } + + final private boolean jj_2_1(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_1(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(0, xla); } + } + + final private boolean jj_3_1() { + if (jj_scan_token(TERM)) return true; + if (jj_scan_token(COLON)) return true; + return false; + } + + public QueryParserTokenManager token_source; + public Token token, jj_nt; + private int jj_ntk; + private Token jj_scanpos, jj_lastpos; + private int jj_la; + public boolean lookingAhead = false; + private boolean jj_semLA; + private int jj_gen; + final private int[] jj_la1 = new int[22]; + static private int[] jj_la1_0; + static { + jj_la1_0(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x180,0x180,0xe00,0xe00,0xfb1f80,0x8000,0xfb1000,0x9a0000,0x40000,0x40000,0x8000,0xc000000,0x1000000,0xc000000,0x8000,0xc0000000,0x10000000,0xc0000000,0x8000,0x40000,0x8000,0xfb0000,}; + } + final private JJCalls[] jj_2_rtns = new JJCalls[1]; + private boolean jj_rescan = false; + private int jj_gc = 0; + + public QueryParser(CharStream stream) { + token_source = new QueryParserTokenManager(stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(CharStream stream) { + token_source.ReInit(stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public QueryParser(QueryParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(QueryParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + if (++jj_gc > 100) { + jj_gc = 0; + for (int i = 0; i < jj_2_rtns.length; i++) { + JJCalls c = jj_2_rtns[i]; + while (c != null) { + if (c.gen < jj_gen) c.first = null; + c = c.next; + } + } + } + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + static private final class LookaheadSuccess extends java.lang.Error { } + final private LookaheadSuccess jj_ls = new LookaheadSuccess(); + final private boolean jj_scan_token(int kind) { + if (jj_scanpos == jj_lastpos) { + jj_la--; + if (jj_scanpos.next == null) { + jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken(); + } else { + jj_lastpos = jj_scanpos = jj_scanpos.next; + } + } else { + jj_scanpos = jj_scanpos.next; + } + if (jj_rescan) { + int i = 0; Token tok = token; + while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; } + if (tok != null) jj_add_error_token(kind, i); + } + if (jj_scanpos.kind != kind) return true; + if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls; + return false; + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = lookingAhead ? jj_scanpos : token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector jj_expentries = new java.util.Vector(); + private int[] jj_expentry; + private int jj_kind = -1; + private int[] jj_lasttokens = new int[100]; + private int jj_endpos; + + private void jj_add_error_token(int kind, int pos) { + if (pos >= 100) return; + if (pos == jj_endpos + 1) { + jj_lasttokens[jj_endpos++] = kind; + } else if (jj_endpos != 0) { + jj_expentry = new int[jj_endpos]; + for (int i = 0; i < jj_endpos; i++) { + jj_expentry[i] = jj_lasttokens[i]; + } + boolean exists = false; + for (java.util.Enumeration e = jj_expentries.elements(); e.hasMoreElements();) { + int[] oldentry = (int[])(e.nextElement()); + if (oldentry.length == jj_expentry.length) { + exists = true; + for (int i = 0; i < jj_expentry.length; i++) { + if (oldentry[i] != jj_expentry[i]) { + exists = false; + break; + } + } + if (exists) break; + } + } + if (!exists) jj_expentries.addElement(jj_expentry); + if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind; + } + } + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[32]; + for (int i = 0; i < 32; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 22; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1< jj_gen) { + jj_la = p.arg; jj_lastpos = jj_scanpos = p.first; + switch (i) { + case 0: jj_3_1(); break; + } + } + p = p.next; + } while (p != null); + } + jj_rescan = false; + } + + final private void jj_save(int index, int xla) { + JJCalls p = jj_2_rtns[index]; + while (p.gen > jj_gen) { + if (p.next == null) { p = p.next = new JJCalls(); break; } + p = p.next; + } + p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/QueryParser.jj b/source/java/org/alfresco/repo/search/impl/lucene/QueryParser.jj new file mode 100644 index 0000000000..b5a9c4350c --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/QueryParser.jj @@ -0,0 +1,826 @@ +/** + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +options { + STATIC=false; + JAVA_UNICODE_ESCAPE=true; + USER_CHAR_STREAM=true; +} + +PARSER_BEGIN(QueryParser) + +package org.alfresco.repo.search.impl.lucene; + +import java.util.Vector; +import java.io.*; +import java.text.*; +import java.util.*; +import org.apache.lucene.index.Term; +import org.apache.lucene.analysis.*; +import org.apache.lucene.document.*; +import org.apache.lucene.search.*; + +/** + * This class is generated by JavaCC. The only method that clients should need + * to call is parse(). + * + * The syntax for query strings is as follows: + * A Query is a series of clauses. + * A clause may be prefixed by: + *

      + *
    • a plus (+) or a minus (-) sign, indicating + * that the clause is required or prohibited respectively; or + *
    • a term followed by a colon, indicating the field to be searched. + * This enables one to construct queries which search multiple fields. + *
    + * + * A clause may be either: + *
      + *
    • a term, indicating all the documents that contain this term; or + *
    • a nested query, enclosed in parentheses. Note that this may be used + * with a +/- prefix to require any of a set of + * terms. + *
    + * + * Thus, in BNF, the query grammar is: + *
    + *   Query  ::= ( Clause )*
    + *   Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")" )
    + * 
    + * + *

    + * Examples of appropriately formatted queries can be found in the test cases. + *

    + * + * @author Brian Goetz + * @author Peter Halacsy + * @author Tatu Saloranta + */ + +public class QueryParser { + + private static final int CONJ_NONE = 0; + private static final int CONJ_AND = 1; + private static final int CONJ_OR = 2; + + private static final int MOD_NONE = 0; + private static final int MOD_NOT = 10; + private static final int MOD_REQ = 11; + + public static final int DEFAULT_OPERATOR_OR = 0; + public static final int DEFAULT_OPERATOR_AND = 1; + + /** The actual operator that parser uses to combine query terms */ + private int operator = DEFAULT_OPERATOR_OR; + + /** + * Whether terms of wildcard and prefix queries are to be automatically + * lower-cased or not. Default is true. + */ + boolean lowercaseWildcardTerms = true; + + Analyzer analyzer; + String field; + int phraseSlop = 0; + float fuzzyMinSim = FuzzyQuery.defaultMinSimilarity; + Locale locale = Locale.getDefault(); + + /** Parses a query string, returning a {@link org.apache.lucene.search.Query}. + * @param query the query string to be parsed. + * @param field the default field for query terms. + * @param analyzer used to find terms in the query text. + * @throws ParseException if the parsing fails + */ + static public Query parse(String query, String field, Analyzer analyzer) + throws ParseException { + QueryParser parser = new QueryParser(field, analyzer); + return parser.parse(query); + } + + /** Constructs a query parser. + * @param f the default field for query terms. + * @param a used to find terms in the query text. + */ + public QueryParser(String f, Analyzer a) { + this(new FastCharStream(new StringReader(""))); + analyzer = a; + field = f; + } + + /** Parses a query string, returning a + * Query. + * @param query the query string to be parsed. + * @throws ParseException if the parsing fails + */ + public Query parse(String query) throws ParseException { + ReInit(new FastCharStream(new StringReader(query))); + try { + return Query(field); + } + catch (TokenMgrError tme) { + throw new ParseException(tme.getMessage()); + } + catch (BooleanQuery.TooManyClauses tmc) { + throw new ParseException("Too many boolean clauses"); + } + } + + /** + * @return Returns the analyzer. + */ + public Analyzer getAnalyzer() { + return analyzer; + } + + /** + * @return Returns the field. + */ + public String getField() { + return field; + } + + /** + * Get the default minimal similarity for fuzzy queries. + */ + public float getFuzzyMinSim() { + return fuzzyMinSim; + } + /** + *Set the default minimum similarity for fuzzy queries. + */ + public void setFuzzyMinSim(float fuzzyMinSim) { + this.fuzzyMinSim = fuzzyMinSim; + } + + /** + * Sets the default slop for phrases. If zero, then exact phrase matches + * are required. Default value is zero. + */ + public void setPhraseSlop(int phraseSlop) { + this.phraseSlop = phraseSlop; + } + + /** + * Gets the default slop for phrases. + */ + public int getPhraseSlop() { + return phraseSlop; + } + + /** + * Sets the boolean operator of the QueryParser. + * In classic mode (DEFAULT_OPERATOR_OR) terms without any modifiers + * are considered optional: for example capital of Hungary is equal to + * capital OR of OR Hungary.
    + * In DEFAULT_OPERATOR_AND terms are considered to be in conjuction: the + * above mentioned query is parsed as capital AND of AND Hungary + */ + public void setOperator(int operator) { + this.operator = operator; + } + + /** + * Gets implicit operator setting, which will be either DEFAULT_OPERATOR_AND + * or DEFAULT_OPERATOR_OR. + */ + public int getOperator() { + return operator; + } + + public void setLowercaseWildcardTerms(boolean lowercaseWildcardTerms) { + this.lowercaseWildcardTerms = lowercaseWildcardTerms; + } + + public boolean getLowercaseWildcardTerms() { + return lowercaseWildcardTerms; + } + + /** + * Set locale used by date range parsing. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Returns current locale, allowing access by subclasses. + */ + public Locale getLocale() { + return locale; + } + + protected void addClause(Vector clauses, int conj, int mods, Query q) { + boolean required, prohibited; + + // If this term is introduced by AND, make the preceding term required, + // unless it's already prohibited + if (clauses.size() > 0 && conj == CONJ_AND) { + BooleanClause c = (BooleanClause) clauses.elementAt(clauses.size()-1); + if (!c.prohibited) + c.required = true; + } + + if (clauses.size() > 0 && operator == DEFAULT_OPERATOR_AND && conj == CONJ_OR) { + // If this term is introduced by OR, make the preceding term optional, + // unless it's prohibited (that means we leave -a OR b but +a OR b-->a OR b) + // notice if the input is a OR b, first term is parsed as required; without + // this modification a OR b would parsed as +a OR b + BooleanClause c = (BooleanClause) clauses.elementAt(clauses.size()-1); + if (!c.prohibited) + c.required = false; + } + + // We might have been passed a null query; the term might have been + // filtered away by the analyzer. + if (q == null) + return; + + if (operator == DEFAULT_OPERATOR_OR) { + // We set REQUIRED if we're introduced by AND or +; PROHIBITED if + // introduced by NOT or -; make sure not to set both. + prohibited = (mods == MOD_NOT); + required = (mods == MOD_REQ); + if (conj == CONJ_AND && !prohibited) { + required = true; + } + } else { + // We set PROHIBITED if we're introduced by NOT or -; We set REQUIRED + // if not PROHIBITED and not introduced by OR + prohibited = (mods == MOD_NOT); + required = (!prohibited && conj != CONJ_OR); + } + clauses.addElement(new BooleanClause(q, required, prohibited)); + } + + /** + * Note that parameter analyzer is ignored. Calls inside the parser always + * use class member analyser. This method will be deprecated and substituted + * by {@link #getFieldQuery(String, String)} in future versions of Lucene. + * Currently overwriting either of these methods works. + * + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, + Analyzer analyzer, + String queryText) throws ParseException { + return getFieldQuery(field, queryText); + } + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText) throws ParseException { + // Use the analyzer to get all the tokens, and then build a TermQuery, + // PhraseQuery, or nothing based on the term count + + TokenStream source = analyzer.tokenStream(field, + new StringReader(queryText)); + Vector v = new Vector(); + org.apache.lucene.analysis.Token t; + + while (true) { + try { + t = source.next(); + } + catch (IOException e) { + t = null; + } + if (t == null) + break; + v.addElement(t.termText()); + } + try { + source.close(); + } + catch (IOException e) { + // ignore + } + + if (v.size() == 0) + return null; + else if (v.size() == 1) + return new TermQuery(new Term(field, (String) v.elementAt(0))); + else { + PhraseQuery q = new PhraseQuery(); + q.setSlop(phraseSlop); + for (int i=0; i + * Depending on settings, prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

    + * Can be overridden by extending classes, to provide custom handling for + * wildcard queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token that contains one or more wild card + * characters (? or *), but is not simple prefix term + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getWildcardQuery(String field, String termStr) throws ParseException + { + if (lowercaseWildcardTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return new WildcardQuery(t); + } + + /** + * Factory method for generating a query (similar to + * ({@link #getWildcardQuery}). Called when parser parses an input term + * token that uses prefix notation; that is, contains a single '*' wildcard + * character as its last character. Since this is a special case + * of generic wildcard term, and such a query can be optimized easily, + * this usually results in a different query object. + *

    + * Depending on settings, a prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

    + * Can be overridden by extending classes, to provide custom handling for + * wild card queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * (without trailing '*' character!) + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getPrefixQuery(String field, String termStr) throws ParseException + { + if (lowercaseWildcardTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return new PrefixQuery(t); + } + + /** + * Factory method for generating a query (similar to + * ({@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr) throws ParseException { + return getFuzzyQuery(field, termStr, fuzzyMinSim); + } + + /** + * Factory method for generating a query (similar to + * ({@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~floatNumber) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * @param minSimilarity the minimum similarity required for a fuzzy match + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException + { + Term t = new Term(field, termStr); + return new FuzzyQuery(t, minSimilarity); + } + + /** + * Returns a String where the escape char has been + * removed, or kept only once if there was a double escape. + */ + private String discardEscapeChar(String input) { + char[] caSource = input.toCharArray(); + char[] caDest = new char[caSource.length]; + int j = 0; + for (int i = 0; i < caSource.length; i++) { + if ((caSource[i] != '\\') || (i > 0 && caSource[i-1] == '\\')) { + caDest[j++]=caSource[i]; + } + } + return new String(caDest, 0, j); + } + + /** + * Returns a String where those characters that QueryParser + * expects to be escaped are escaped, i.e. preceded by a \. + */ + public static String escape(String s) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // NOTE: keep this in sync with _ESCAPED_CHAR below! + if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' + || c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~' + || c == '*' || c == '?') { + sb.append('\\'); + } + sb.append(c); + } + return sb.toString(); + } + + public static void main(String[] args) throws Exception { + QueryParser qp = new QueryParser("field", + new org.apache.lucene.analysis.SimpleAnalyzer()); + Query q = qp.parse(args[0]); + System.out.println(q.toString("field")); + } +} + +PARSER_END(QueryParser) + +/* ***************** */ +/* Token Definitions */ +/* ***************** */ + +<*> TOKEN : { + <#_NUM_CHAR: ["0"-"9"] > +// NOTE: keep this in sync with escape(String) above! +| <#_ESCAPED_CHAR: "\\" [ "\\", "+", "-", "!", "(", ")", ":", "^", + "[", "]", "\"", "{", "}", "~", "*", "?" ] > +| <#_TERM_START_CHAR: ( ~[ " ", "\t", "\n", "\r", "+", "-", "!", "(", ")", ":", "^", + "[", "]", "\"", "{", "}", "~", "*", "?" ] + | <_ESCAPED_CHAR> ) > +| <#_TERM_CHAR: ( <_TERM_START_CHAR> | <_ESCAPED_CHAR> | "-" | "+" ) > +| <#_WHITESPACE: ( " " | "\t" | "\n" | "\r") > +} + + SKIP : { + <<_WHITESPACE>> +} + +// OG: to support prefix queries: +// http://nagoya.apache.org/bugzilla/show_bug.cgi?id=12137 +// Change from: +// | +// (<_TERM_CHAR> | ( [ "*", "?" ] ))* > +// To: +// +// | | ( [ "*", "?" ] ))* > + + TOKEN : { + +| +| +| +| +| +| +| +| : Boost +| +| (<_TERM_CHAR>)* > +| )+ ( "." (<_NUM_CHAR>)+ )? )? > +| (<_TERM_CHAR>)* "*" > +| | ( [ "*", "?" ] )) + (<_TERM_CHAR> | ( [ "*", "?" ] ))* > +| : RangeIn +| : RangeEx +} + + TOKEN : { +)+ ( "." (<_NUM_CHAR>)+ )? > : DEFAULT +} + + TOKEN : { + +| : DEFAULT +| +| +} + + TOKEN : { + +| : DEFAULT +| +| +} + +// * Query ::= ( Clause )* +// * Clause ::= ["+", "-"] [ ":"] ( | "(" Query ")" ) + +int Conjunction() : { + int ret = CONJ_NONE; +} +{ + [ + { ret = CONJ_AND; } + | { ret = CONJ_OR; } + ] + { return ret; } +} + +int Modifiers() : { + int ret = MOD_NONE; +} +{ + [ + { ret = MOD_REQ; } + | { ret = MOD_NOT; } + | { ret = MOD_NOT; } + ] + { return ret; } +} + +Query Query(String field) : +{ + Vector clauses = new Vector(); + Query q, firstQuery=null; + int conj, mods; +} +{ + mods=Modifiers() q=Clause(field) + { + addClause(clauses, CONJ_NONE, mods, q); + if (mods == MOD_NONE) + firstQuery=q; + } + ( + conj=Conjunction() mods=Modifiers() q=Clause(field) + { addClause(clauses, conj, mods, q); } + )* + { + if (clauses.size() == 1 && firstQuery != null) + return firstQuery; + else { + return getBooleanQuery(clauses); + } + } +} + +Query Clause(String field) : { + Query q; + Token fieldToken=null, boost=null; +} +{ + [ + LOOKAHEAD(2) + fieldToken= { + field=discardEscapeChar(fieldToken.image); + } + ] + + ( + q=Term(field) + | q=Query(field) ( boost=)? + + ) + { + if (boost != null) { + float f = (float)1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + q.setBoost(f); + } catch (Exception ignored) { } + } + return q; + } +} + + +Query Term(String field) : { + Token term, boost=null, fuzzySlop=null, goop1, goop2; + boolean prefix = false; + boolean wildcard = false; + boolean fuzzy = false; + boolean rangein = false; + Query q; +} +{ + ( + ( + term= + | term= { prefix=true; } + | term= { wildcard=true; } + | term= + ) + [ fuzzySlop= { fuzzy=true; } ] + [ boost= [ fuzzySlop= { fuzzy=true; } ] ] + { + String termImage=discardEscapeChar(term.image); + if (wildcard) { + q = getWildcardQuery(field, termImage); + } else if (prefix) { + q = getPrefixQuery(field, + discardEscapeChar(term.image.substring + (0, term.image.length()-1))); + } else if (fuzzy) { + float fms = fuzzyMinSim; + try { + fms = Float.valueOf(fuzzySlop.image.substring(1)).floatValue(); + } catch (Exception ignored) { } + if(fms < 0.0f || fms > 1.0f){ + throw new ParseException("Minimum similarity for a FuzzyQuery has to be between 0.0f and 1.0f !"); + } + if(fms == fuzzyMinSim) + q = getFuzzyQuery(field, termImage); + else + q = getFuzzyQuery(field, termImage, fms); + } else { + q = getFieldQuery(field, analyzer, termImage); + } + } + | ( ( goop1=|goop1= ) + [ ] ( goop2=|goop2= ) + ) + [ boost= ] + { + if (goop1.kind == RANGEIN_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } else { + goop1.image = discardEscapeChar(goop1.image); + } + if (goop2.kind == RANGEIN_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } else { + goop2.image = discardEscapeChar(goop2.image); + } + q = getRangeQuery(field, analyzer, goop1.image, goop2.image, true); + } + | ( ( goop1=|goop1= ) + [ ] ( goop2=|goop2= ) + ) + [ boost= ] + { + if (goop1.kind == RANGEEX_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } else { + goop1.image = discardEscapeChar(goop1.image); + } + if (goop2.kind == RANGEEX_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } else { + goop2.image = discardEscapeChar(goop2.image); + } + + q = getRangeQuery(field, analyzer, goop1.image, goop2.image, false); + } + | term= + [ fuzzySlop= ] + [ boost= ] + { + int s = phraseSlop; + + if (fuzzySlop != null) { + try { + s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { } + } + q = getFieldQuery(field, analyzer, term.image.substring(1, term.image.length()-1), s); + } + ) + { + if (boost != null) { + float f = (float) 1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + } + catch (Exception ignored) { + /* Should this be handled somehow? (defaults to "no boost", if + * boost number is invalid) + */ + } + + // avoid boosting null queries, such as those caused by stop words + if (q != null) { + q.setBoost(f); + } + } + return q; + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/QueryParserConstants.java b/source/java/org/alfresco/repo/search/impl/lucene/QueryParserConstants.java new file mode 100644 index 0000000000..a60f36a9d6 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/QueryParserConstants.java @@ -0,0 +1,78 @@ +/* Generated By:JavaCC: Do not edit this line. QueryParserConstants.java */ +package org.alfresco.repo.search.impl.lucene; + +public interface QueryParserConstants { + + int EOF = 0; + int _NUM_CHAR = 1; + int _ESCAPED_CHAR = 2; + int _TERM_START_CHAR = 3; + int _TERM_CHAR = 4; + int _WHITESPACE = 5; + int AND = 7; + int OR = 8; + int NOT = 9; + int PLUS = 10; + int MINUS = 11; + int LPAREN = 12; + int RPAREN = 13; + int COLON = 14; + int CARAT = 15; + int QUOTED = 16; + int TERM = 17; + int FUZZY_SLOP = 18; + int PREFIXTERM = 19; + int WILDTERM = 20; + int RANGEIN_START = 21; + int RANGEEX_START = 22; + int NUMBER = 23; + int RANGEIN_TO = 24; + int RANGEIN_END = 25; + int RANGEIN_QUOTED = 26; + int RANGEIN_GOOP = 27; + int RANGEEX_TO = 28; + int RANGEEX_END = 29; + int RANGEEX_QUOTED = 30; + int RANGEEX_GOOP = 31; + + int Boost = 0; + int RangeEx = 1; + int RangeIn = 2; + int DEFAULT = 3; + + String[] tokenImage = { + "", + "<_NUM_CHAR>", + "<_ESCAPED_CHAR>", + "<_TERM_START_CHAR>", + "<_TERM_CHAR>", + "<_WHITESPACE>", + "", + "", + "", + "", + "\"+\"", + "\"-\"", + "\"(\"", + "\")\"", + "\":\"", + "\"^\"", + "", + "", + "", + "", + "", + "\"[\"", + "\"{\"", + "", + "\"TO\"", + "\"]\"", + "", + "", + "\"TO\"", + "\"}\"", + "", + "", + }; + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/QueryParserTokenManager.java b/source/java/org/alfresco/repo/search/impl/lucene/QueryParserTokenManager.java new file mode 100644 index 0000000000..c7f66313ed --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/QueryParserTokenManager.java @@ -0,0 +1,1081 @@ +/* Generated By:JavaCC: Do not edit this line. QueryParserTokenManager.java */ +package org.alfresco.repo.search.impl.lucene; + +public class QueryParserTokenManager implements QueryParserConstants +{ + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 12); + case 41: + return jjStopAtPos(0, 13); + case 43: + return jjStopAtPos(0, 10); + case 45: + return jjStopAtPos(0, 11); + case 58: + return jjStopAtPos(0, 14); + case 91: + return jjStopAtPos(0, 21); + case 94: + return jjStopAtPos(0, 15); + case 123: + return jjStopAtPos(0, 22); + default : + return jjMoveNfa_3(0, 0); + } +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +static final long[] jjbitVec0 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec2 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_3(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 34; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xfbffd4f8ffffd9ffL & l) != 0L) + { + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + } + else if ((0x100002600L & l) != 0L) + { + if (kind > 6) + kind = 6; + } + else if (curChar == 34) + jjCheckNAdd(15); + else if (curChar == 33) + { + if (kind > 9) + kind = 9; + } + if ((0x7bffd0f8ffffd9ffL & l) != 0L) + { + if (kind > 17) + kind = 17; + jjCheckNAddStates(0, 4); + } + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 4: + if (curChar == 38 && kind > 7) + kind = 7; + break; + case 5: + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 13: + if (curChar == 33 && kind > 9) + kind = 9; + break; + case 14: + if (curChar == 34) + jjCheckNAdd(15); + break; + case 15: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(15, 16); + break; + case 16: + if (curChar == 34 && kind > 16) + kind = 16; + break; + case 18: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjAddStates(5, 6); + break; + case 19: + if (curChar == 46) + jjCheckNAdd(20); + break; + case 20: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(20); + break; + case 21: + if ((0xfbffd4f8ffffd9ffL & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + break; + case 22: + if ((0xfbfffcf8ffffd9ffL & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + break; + case 24: + if ((0x84002f0600000000L & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + break; + case 25: + if ((0x7bffd0f8ffffd9ffL & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAddStates(0, 4); + break; + case 26: + if ((0x7bfff8f8ffffd9ffL & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAddTwoStates(26, 27); + break; + case 28: + if ((0x84002f0600000000L & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAddTwoStates(26, 27); + break; + case 29: + if ((0x7bfff8f8ffffd9ffL & l) != 0L) + jjCheckNAddStates(7, 9); + break; + case 30: + if (curChar == 42 && kind > 19) + kind = 19; + break; + case 32: + if ((0x84002f0600000000L & l) != 0L) + jjCheckNAddStates(7, 9); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x97ffffff97ffffffL & l) != 0L) + { + if (kind > 17) + kind = 17; + jjCheckNAddStates(0, 4); + } + else if (curChar == 126) + { + if (kind > 18) + kind = 18; + jjstateSet[jjnewStateCnt++] = 18; + } + if ((0x97ffffff97ffffffL & l) != 0L) + { + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + } + if (curChar == 92) + jjCheckNAddStates(10, 12); + else if (curChar == 78) + jjstateSet[jjnewStateCnt++] = 11; + else if (curChar == 124) + jjstateSet[jjnewStateCnt++] = 8; + else if (curChar == 79) + jjstateSet[jjnewStateCnt++] = 6; + else if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 2; + break; + case 1: + if (curChar == 68 && kind > 7) + kind = 7; + break; + case 2: + if (curChar == 78) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 3: + if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 2; + break; + case 6: + if (curChar == 82 && kind > 8) + kind = 8; + break; + case 7: + if (curChar == 79) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 8: + if (curChar == 124 && kind > 8) + kind = 8; + break; + case 9: + if (curChar == 124) + jjstateSet[jjnewStateCnt++] = 8; + break; + case 10: + if (curChar == 84 && kind > 9) + kind = 9; + break; + case 11: + if (curChar == 79) + jjstateSet[jjnewStateCnt++] = 10; + break; + case 12: + if (curChar == 78) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 15: + jjAddStates(13, 14); + break; + case 17: + if (curChar != 126) + break; + if (kind > 18) + kind = 18; + jjstateSet[jjnewStateCnt++] = 18; + break; + case 21: + case 22: + if ((0x97ffffff97ffffffL & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + break; + case 23: + if (curChar == 92) + jjCheckNAddTwoStates(24, 24); + break; + case 24: + if ((0x6800000078000000L & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + break; + case 25: + if ((0x97ffffff97ffffffL & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAddStates(0, 4); + break; + case 26: + if ((0x97ffffff97ffffffL & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAddTwoStates(26, 27); + break; + case 27: + if (curChar == 92) + jjCheckNAddTwoStates(28, 28); + break; + case 28: + if ((0x6800000078000000L & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAddTwoStates(26, 27); + break; + case 29: + if ((0x97ffffff97ffffffL & l) != 0L) + jjCheckNAddStates(7, 9); + break; + case 31: + if (curChar == 92) + jjCheckNAddTwoStates(32, 32); + break; + case 32: + if ((0x6800000078000000L & l) != 0L) + jjCheckNAddStates(7, 9); + break; + case 33: + if (curChar == 92) + jjCheckNAddStates(10, 12); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + } + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { + if (kind > 17) + kind = 17; + jjCheckNAddStates(0, 4); + } + break; + case 15: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(13, 14); + break; + case 21: + case 22: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 20) + kind = 20; + jjCheckNAddTwoStates(22, 23); + break; + case 25: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 17) + kind = 17; + jjCheckNAddStates(0, 4); + break; + case 26: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 17) + kind = 17; + jjCheckNAddTwoStates(26, 27); + break; + case 29: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(7, 9); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 34 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x10000000L) != 0L) + { + jjmatchedKind = 31; + return 4; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 84: + return jjMoveStringLiteralDfa1_1(0x10000000L); + case 125: + return jjStopAtPos(0, 29); + default : + return jjMoveNfa_1(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_1(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(0, active0); + return 1; + } + switch(curChar) + { + case 79: + if ((active0 & 0x10000000L) != 0L) + return jjStartNfaWithStates_1(1, 28, 4); + break; + default : + break; + } + return jjStartNfa_1(0, active0); +} +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 5; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xfffffffeffffffffL & l) != 0L) + { + if (kind > 31) + kind = 31; + jjCheckNAdd(4); + } + if ((0x100002600L & l) != 0L) + { + if (kind > 6) + kind = 6; + } + else if (curChar == 34) + jjCheckNAdd(2); + break; + case 1: + if (curChar == 34) + jjCheckNAdd(2); + break; + case 2: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 3: + if (curChar == 34 && kind > 30) + kind = 30; + break; + case 4: + if ((0xfffffffeffffffffL & l) == 0L) + break; + if (kind > 31) + kind = 31; + jjCheckNAdd(4); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 4: + if ((0xdfffffffffffffffL & l) == 0L) + break; + if (kind > 31) + kind = 31; + jjCheckNAdd(4); + break; + case 2: + jjAddStates(15, 16); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 31) + kind = 31; + jjCheckNAdd(4); + break; + case 2: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(15, 16); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 5 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjMoveStringLiteralDfa0_0() +{ + return jjMoveNfa_0(0, 0); +} +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 23) + kind = 23; + jjAddStates(17, 18); + break; + case 1: + if (curChar == 46) + jjCheckNAdd(2); + break; + case 2: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 23) + kind = 23; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x1000000L) != 0L) + { + jjmatchedKind = 27; + return 4; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 84: + return jjMoveStringLiteralDfa1_2(0x1000000L); + case 93: + return jjStopAtPos(0, 25); + default : + return jjMoveNfa_2(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_2(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(0, active0); + return 1; + } + switch(curChar) + { + case 79: + if ((active0 & 0x1000000L) != 0L) + return jjStartNfaWithStates_2(1, 24, 4); + break; + default : + break; + } + return jjStartNfa_2(0, active0); +} +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 5; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xfffffffeffffffffL & l) != 0L) + { + if (kind > 27) + kind = 27; + jjCheckNAdd(4); + } + if ((0x100002600L & l) != 0L) + { + if (kind > 6) + kind = 6; + } + else if (curChar == 34) + jjCheckNAdd(2); + break; + case 1: + if (curChar == 34) + jjCheckNAdd(2); + break; + case 2: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 3: + if (curChar == 34 && kind > 26) + kind = 26; + break; + case 4: + if ((0xfffffffeffffffffL & l) == 0L) + break; + if (kind > 27) + kind = 27; + jjCheckNAdd(4); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 4: + if ((0xffffffffdfffffffL & l) == 0L) + break; + if (kind > 27) + kind = 27; + jjCheckNAdd(4); + break; + case 2: + jjAddStates(15, 16); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 27) + kind = 27; + jjCheckNAdd(4); + break; + case 2: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(15, 16); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 5 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { + 26, 29, 30, 31, 27, 18, 19, 29, 30, 31, 28, 32, 24, 15, 16, 2, + 3, 0, 1, +}; +private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec2[i2] & l2) != 0L); + default : + if ((jjbitVec0[i1] & l1) != 0L) + return true; + return false; + } +} +public static final String[] jjstrLiteralImages = { +"", null, null, null, null, null, null, null, null, null, "\53", "\55", "\50", +"\51", "\72", "\136", null, null, null, null, null, "\133", "\173", null, "\124\117", +"\135", null, null, "\124\117", "\175", null, null, }; +public static final String[] lexStateNames = { + "Boost", + "RangeEx", + "RangeIn", + "DEFAULT", +}; +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, 2, 1, 3, -1, + 3, -1, -1, -1, 3, -1, -1, +}; +static final long[] jjtoToken = { + 0xffffff81L, +}; +static final long[] jjtoSkip = { + 0x40L, +}; +protected CharStream input_stream; +private final int[] jjrounds = new int[34]; +private final int[] jjstateSet = new int[68]; +protected char curChar; +public QueryParserTokenManager(CharStream stream) +{ + input_stream = stream; +} +public QueryParserTokenManager(CharStream stream, int lexState) +{ + this(stream); + SwitchTo(lexState); +} +public void ReInit(CharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 34; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(CharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 4 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 3; +int defaultLexState = 3; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + return matchedToken; + } + + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else + { + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } +} + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/Token.java b/source/java/org/alfresco/repo/search/impl/lucene/Token.java new file mode 100644 index 0000000000..57cefb2b54 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/Token.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +package org.alfresco.repo.search.impl.lucene; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/TokenMgrError.java b/source/java/org/alfresco/repo/search/impl/lucene/TokenMgrError.java new file mode 100644 index 0000000000..452f2183eb --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/TokenMgrError.java @@ -0,0 +1,133 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +package org.alfresco.repo.search.impl.lucene; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuilder retval = new StringBuilder(str.length() + 8); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/CategoryAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/CategoryAnalyser.java new file mode 100644 index 0000000000..dd6d6f055f --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/CategoryAnalyser.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +/** + * @author andyh + * + * TODO To change the template for this generated type comment go to Window - + * Preferences - Java - Code Style - Code Templates + */ +public class CategoryAnalyser extends Analyzer +{ + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.Analyzer#tokenStream(java.lang.String, + * java.io.Reader) + */ + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new PathTokenFilter(reader, PathTokenFilter.PATH_SEPARATOR, + PathTokenFilter.SEPARATOR_TOKEN_TEXT, PathTokenFilter.NO_NS_TOKEN_TEXT, + PathTokenFilter.NAMESPACE_START_DELIMITER, PathTokenFilter.NAMESPACE_END_DELIMITER, false); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/DateAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DateAnalyser.java new file mode 100644 index 0000000000..97ed5fbbce --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DateAnalyser.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +public class DateAnalyser extends Analyzer +{ + + public DateAnalyser() + { + super(); + } + + // Split at the T in the XML date form + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new DateTokenFilter(reader); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/DateTokenFilter.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DateTokenFilter.java new file mode 100644 index 0000000000..f078a79996 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DateTokenFilter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.IOException; +import java.io.Reader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.alfresco.util.CachingDateFormat; +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.WhitespaceTokenizer; + +/** + * @author andyh + */ +public class DateTokenFilter extends Tokenizer +{ + Tokenizer baseTokeniser; + + public DateTokenFilter(Reader in) + { + super(in); + baseTokeniser = new WhitespaceTokenizer(in); + } + + public Token next() throws IOException + { + SimpleDateFormat df = CachingDateFormat.getDateFormat(); + SimpleDateFormat dof = CachingDateFormat.getDateOnlyFormat(); + Token candidate; + while((candidate = baseTokeniser.next()) != null) + { + Date date; + try + { + date = df.parse(candidate.termText()); + } + catch (ParseException e) + { + continue; + } + String valueString = dof.format(date); + Token integerToken = new Token(valueString, candidate.startOffset(), candidate.startOffset(), + candidate.type()); + return integerToken; + } + return null; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleAnalyser.java new file mode 100644 index 0000000000..3161c6a508 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleAnalyser.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +/** + * Simple analyser to wrap the tokenisation of doubles. + * + * @author Andy Hind + */ +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +public class DoubleAnalyser extends Analyzer +{ + + public DoubleAnalyser() + { + super(); + } + + + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new DoubleTokenFilter(reader); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleTokenFilter.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleTokenFilter.java new file mode 100644 index 0000000000..ae87ed6e6b --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/DoubleTokenFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +/** + * Simple tokeniser for doubles. + * + * @author Andy Hind + */ +public class DoubleTokenFilter extends Tokenizer +{ + Tokenizer baseTokeniser; + + public DoubleTokenFilter(Reader in) + { + super(in); + baseTokeniser = new StandardTokenizer(in); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.TokenStream#next() + */ + + public Token next() throws IOException + { + Token candidate; + while((candidate = baseTokeniser.next()) != null) + { + Double d = Double.valueOf(candidate.termText()); + String valueString = NumericEncoder.encode(d.doubleValue()); + Token doubleToken = new Token(valueString, candidate.startOffset(), candidate.startOffset(), + candidate.type()); + return doubleToken; + } + return null; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatAnalyser.java new file mode 100644 index 0000000000..1ac85c9265 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatAnalyser.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +/** + * Simple analyser for floats. + * + * @author Andy Hind + */ +public class FloatAnalyser extends Analyzer +{ + + public FloatAnalyser() + { + super(); + } + + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new FloatTokenFilter(reader); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatTokenFilter.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatTokenFilter.java new file mode 100644 index 0000000000..387707e1b6 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/FloatTokenFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +/** + * Simple tokeniser for floats. + * + * @author Andy Hind + */ +public class FloatTokenFilter extends Tokenizer +{ + Tokenizer baseTokeniser; + + public FloatTokenFilter(Reader in) + { + super(in); + baseTokeniser = new StandardTokenizer(in); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.TokenStream#next() + */ + + public Token next() throws IOException + { + Token candidate; + while((candidate = baseTokeniser.next()) != null) + { + Float floatValue = Float.valueOf(candidate.termText()); + String valueString = NumericEncoder.encode(floatValue.floatValue()); + Token floatToken = new Token(valueString, candidate.startOffset(), candidate.startOffset(), + candidate.type()); + return floatToken; + } + return null; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerAnalyser.java new file mode 100644 index 0000000000..58b502ab2a --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerAnalyser.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +/** + * Simple analyser for integers. + * + * @author Andy Hind + */ +public class IntegerAnalyser extends Analyzer +{ + + public IntegerAnalyser() + { + super(); + } + + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new IntegerTokenFilter(reader); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerTokenFilter.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerTokenFilter.java new file mode 100644 index 0000000000..0531e78394 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/IntegerTokenFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +/** + * Simple tokeniser for integers. + * + * @author Andy Hind + */ +public class IntegerTokenFilter extends Tokenizer +{ + Tokenizer baseTokeniser; + + public IntegerTokenFilter(Reader in) + { + super(in); + baseTokeniser = new StandardTokenizer(in); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.TokenStream#next() + */ + + public Token next() throws IOException + { + Token candidate; + while((candidate = baseTokeniser.next()) != null) + { + Integer integer = Integer.valueOf(candidate.termText()); + String valueString = NumericEncoder.encode(integer.intValue()); + Token integerToken = new Token(valueString, candidate.startOffset(), candidate.startOffset(), + candidate.type()); + return integerToken; + } + return null; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/LongAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/LongAnalyser.java new file mode 100644 index 0000000000..1ddd318de8 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/LongAnalyser.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +/** + * Simple analyser for longs. + * + * @author Andy Hind + */ +public class LongAnalyser extends Analyzer +{ + + public LongAnalyser() + { + super(); + } + + + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new LongTokenFilter(reader); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/LongTokenFilter.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/LongTokenFilter.java new file mode 100644 index 0000000000..79e00d9326 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/LongTokenFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +/** + * Simple tokeniser for longs. + * + * @author Andy Hind + */ +public class LongTokenFilter extends Tokenizer +{ + Tokenizer baseTokeniser; + + public LongTokenFilter(Reader in) + { + super(in); + baseTokeniser = new StandardTokenizer(in); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.TokenStream#next() + */ + + public Token next() throws IOException + { + Token candidate; + while((candidate = baseTokeniser.next()) != null) + { + Long longValue = Long.valueOf(candidate.termText()); + String valueString = NumericEncoder.encode(longValue.longValue()); + Token longToken = new Token(valueString, candidate.startOffset(), candidate.startOffset(), + candidate.type()); + return longToken; + } + return null; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncoder.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncoder.java new file mode 100644 index 0000000000..785a01e6aa --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncoder.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +/** + * Support to encode numeric types in the lucene index. + * + * To support range queries in the lucene index numeric types need to be indexed + * specially. This has been addressed for int and long types for lucene and + * limited support (via scaling) for float and double. + * + * The implementation splits an int, long, float or double into the sign bit, + * optional exponent and mantissa either from the int or long format or its IEEE + * 754 byte representation. + * + * To index content so small negative numbers are indexed correctly and are + * after big negative numbers in range queries. + * + * The algorithm finds the sign, if the number is negative, then the mantissa + * and exponent are XORed against the appropriate masks. This reverses the + * order. As negative numbers appear first in the list their sign bit is 0 and + * positive numbers are 1. + * + * @author Andy Hind + */ +public class NumericEncoder +{ + /* + * Constants for integer encoding + */ + + static int INTEGER_SIGN_MASK = 0x80000000; + + /* + * Constants for long encoding + */ + + static long LONG_SIGN_MASK = 0x8000000000000000L; + + /* + * Constants for float encoding + */ + + static int FLOAT_SIGN_MASK = 0x80000000; + + static int FLOAT_EXPONENT_MASK = 0x7F800000; + + static int FLOAT_MANTISSA_MASK = 0x007FFFFF; + + /* + * Constants for double encoding + */ + + static long DOUBLE_SIGN_MASK = 0x8000000000000000L; + + static long DOUBLE_EXPONENT_MASK = 0x7FF0000000000000L; + + static long DOUBLE_MANTISSA_MASK = 0x000FFFFFFFFFFFFFL; + + private NumericEncoder() + { + super(); + } + + /** + * Encode an integer into a string that orders correctly using string + * comparison Integer.MIN_VALUE encodes as 00000000 and MAX_VALUE as + * ffffffff. + * + * @param intToEncode + * @return + */ + public static String encode(int intToEncode) + { + int replacement = intToEncode ^ INTEGER_SIGN_MASK; + return encodeToHex(replacement); + } + + /** + * Encode a long into a string that orders correctly using string comparison + * Long.MIN_VALUE encodes as 0000000000000000 and MAX_VALUE as + * ffffffffffffffff. + * + * @param longToEncode + * @return + */ + public static String encode(long longToEncode) + { + long replacement = longToEncode ^ LONG_SIGN_MASK; + return encodeToHex(replacement); + } + + /** + * Encode a float into a string that orders correctly according to string + * comparison. Note that there is no negative NaN but there are codings that + * imply this. So NaN and -Infinity may not compare as expected. + * + * @param floatToEncode + * @return + */ + public static String encode(float floatToEncode) + { + int bits = Float.floatToIntBits(floatToEncode); + int sign = bits & FLOAT_SIGN_MASK; + int exponent = bits & FLOAT_EXPONENT_MASK; + int mantissa = bits & FLOAT_MANTISSA_MASK; + if (sign != 0) + { + exponent ^= FLOAT_EXPONENT_MASK; + mantissa ^= FLOAT_MANTISSA_MASK; + } + sign ^= FLOAT_SIGN_MASK; + int replacement = sign | exponent | mantissa; + return encodeToHex(replacement); + } + + /** + * Encode a double into a string that orders correctly according to string + * comparison. Note that there is no negative NaN but there are codings that + * imply this. So NaN and -Infinity may not compare as expected. + * + * @param doubleToEncode + * @return + */ + public static String encode(double doubleToEncode) + { + long bits = Double.doubleToLongBits(doubleToEncode); + long sign = bits & DOUBLE_SIGN_MASK; + long exponent = bits & DOUBLE_EXPONENT_MASK; + long mantissa = bits & DOUBLE_MANTISSA_MASK; + if (sign != 0) + { + exponent ^= DOUBLE_EXPONENT_MASK; + mantissa ^= DOUBLE_MANTISSA_MASK; + } + sign ^= DOUBLE_SIGN_MASK; + long replacement = sign | exponent | mantissa; + return encodeToHex(replacement); + } + + private static String encodeToHex(int i) + { + char[] buf = new char[] { '0', '0', '0', '0', '0', '0', '0', '0' }; + int charPos = 8; + do + { + buf[--charPos] = DIGITS[i & MASK]; + i >>>= 4; + } + while (i != 0); + return new String(buf); + } + + private static String encodeToHex(long l) + { + char[] buf = new char[] { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0' }; + int charPos = 16; + do + { + buf[--charPos] = DIGITS[(int) l & MASK]; + l >>>= 4; + } + while (l != 0); + return new String(buf); + } + + private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', + 'f' }; + + private static final int MASK = (1 << 4) - 1; +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncodingTest.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncodingTest.java new file mode 100644 index 0000000000..3b4cc73d58 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/NumericEncodingTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import junit.framework.TestCase; + +public class NumericEncodingTest extends TestCase +{ + + public NumericEncodingTest() + { + super(); + } + + public NumericEncodingTest(String arg0) + { + super(arg0); + } + + /** + * Do an exhaustive test for integers + * + */ + public void xtestAllIntegerEncodings() + { + String lastString = null; + String nextString = null; + for (long i = Integer.MIN_VALUE; i <= Integer.MAX_VALUE; i++) + { + nextString = NumericEncoder.encode((int) i); + if (lastString != null) + { + assertFalse(lastString.compareTo(nextString) > 0); + } + lastString = nextString; + } + } + + /** + * Do an exhaustive test for float + * + */ + public void xtestAllFloatEncodings() + { + Float last = null; + Float next = null; + String lastString = null; + String nextString = null; + + for (int sign = 1; sign >= 0; sign--) + { + if (sign == 0) + { + for (int exponent = 0; exponent <= 0xFF; exponent++) + { + for (int mantissa = 0; mantissa <= 0x007FFFFF; mantissa++) + { + int bitPattern = sign << 31 | exponent << 23 | mantissa; + next = Float.intBitsToFloat(bitPattern); + + if (!next.equals(Float.NaN) && (last != null) && (last.compareTo(next) > 0)) + { + System.err.println(last + " > " + next); + } + if (!next.equals(Float.NaN)) + { + nextString = NumericEncoder.encode(next); + if ((lastString != null) && (lastString.compareTo(nextString) > 0)) + { + System.err.println(lastString + " > " + nextString); + } + lastString = nextString; + } + last = next; + + } + } + } + else + { + for (int exponent = 0xFF; exponent >= 0; exponent--) + { + for (int mantissa = 0x007FFFFF; mantissa >= 0; mantissa--) + { + int bitPattern = sign << 31 | exponent << 23 | mantissa; + next = Float.intBitsToFloat(bitPattern); + if (!next.equals(Float.NaN) && (last != null) && (last.compareTo(next) > 0)) + { + System.err.println(last + " > " + next); + } + if (!next.equals(Float.NaN)) + { + nextString = NumericEncoder.encode(next); + if ((lastString != null) && (lastString.compareTo(nextString) > 0)) + { + System.err.println(lastString + " > " + nextString); + } + lastString = nextString; + } + last = next; + } + } + } + } + } + + /* + * Sample test for int + */ + + public void testIntegerEncoding() + { + assertEquals("00000000", NumericEncoder.encode(Integer.MIN_VALUE)); + assertEquals("00000001", NumericEncoder.encode(Integer.MIN_VALUE + 1)); + assertEquals("7fffffff", NumericEncoder.encode(-1)); + assertEquals("80000000", NumericEncoder.encode(0)); + assertEquals("80000001", NumericEncoder.encode(1)); + assertEquals("fffffffe", NumericEncoder.encode(Integer.MAX_VALUE - 1)); + assertEquals("ffffffff", NumericEncoder.encode(Integer.MAX_VALUE)); + } + + /* + * Sample test for long + */ + + public void testLongEncoding() + { + assertEquals("0000000000000000", NumericEncoder.encode(Long.MIN_VALUE)); + assertEquals("0000000000000001", NumericEncoder.encode(Long.MIN_VALUE + 1)); + assertEquals("7fffffffffffffff", NumericEncoder.encode(-1L)); + assertEquals("8000000000000000", NumericEncoder.encode(0L)); + assertEquals("8000000000000001", NumericEncoder.encode(1L)); + assertEquals("fffffffffffffffe", NumericEncoder.encode(Long.MAX_VALUE - 1)); + assertEquals("ffffffffffffffff", NumericEncoder.encode(Long.MAX_VALUE)); + } + + /* + * Sample test for float + */ + + public void testFloatEncoding() + { + assertEquals("007fffff", NumericEncoder.encode(Float.NEGATIVE_INFINITY)); + assertEquals("00800000", NumericEncoder.encode(-Float.MAX_VALUE)); + assertEquals("7ffffffe", NumericEncoder.encode(-Float.MIN_VALUE)); + assertEquals("7fffffff", NumericEncoder.encode(-0f)); + assertEquals("80000000", NumericEncoder.encode(0f)); + assertEquals("80000001", NumericEncoder.encode(Float.MIN_VALUE)); + assertEquals("ff7fffff", NumericEncoder.encode(Float.MAX_VALUE)); + assertEquals("ff800000", NumericEncoder.encode(Float.POSITIVE_INFINITY)); + assertEquals("ffc00000", NumericEncoder.encode(Float.NaN)); + + } + + /* + * Sample test for double + */ + + public void testDoubleEncoding() + { + assertEquals("000fffffffffffff", NumericEncoder.encode(Double.NEGATIVE_INFINITY)); + assertEquals("0010000000000000", NumericEncoder.encode(-Double.MAX_VALUE)); + assertEquals("7ffffffffffffffe", NumericEncoder.encode(-Double.MIN_VALUE)); + assertEquals("7fffffffffffffff", NumericEncoder.encode(-0d)); + assertEquals("8000000000000000", NumericEncoder.encode(0d)); + assertEquals("8000000000000001", NumericEncoder.encode(Double.MIN_VALUE)); + assertEquals("ffefffffffffffff", NumericEncoder.encode(Double.MAX_VALUE)); + assertEquals("fff0000000000000", NumericEncoder.encode(Double.POSITIVE_INFINITY)); + assertEquals("fff8000000000000", NumericEncoder.encode(Double.NaN)); + + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathAnalyser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathAnalyser.java new file mode 100644 index 0000000000..1992d4ed80 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathAnalyser.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +/** + * Analyse repository paths + * + * @author andyh + */ +public class PathAnalyser extends Analyzer +{ + public TokenStream tokenStream(String fieldName, Reader reader) + { + return new PathTokenFilter(reader, PathTokenFilter.PATH_SEPARATOR, + PathTokenFilter.SEPARATOR_TOKEN_TEXT, PathTokenFilter.NO_NS_TOKEN_TEXT, + PathTokenFilter.NAMESPACE_START_DELIMITER, PathTokenFilter.NAMESPACE_END_DELIMITER, true); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokenFilter.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokenFilter.java new file mode 100644 index 0000000000..ad19ffe6c1 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokenFilter.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.IOException; +import java.io.Reader; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Iterator; +import java.util.LinkedList; + +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.Tokenizer; + +/** + * @author andyh + * + * TODO To change the template for this generated type comment go to Window - + * Preferences - Java - Code Style - Code Templates + */ +public class PathTokenFilter extends Tokenizer +{ + public final static String INTEGER_FORMAT = "0000000000"; + + public final static char PATH_SEPARATOR = ';'; + + public final static char NAMESPACE_START_DELIMITER = '{'; + + public final static char NAMESPACE_END_DELIMITER = '}'; + + public final static String SEPARATOR_TOKEN_TEXT = ";"; + + public final static String NO_NS_TOKEN_TEXT = ""; + + public final static String TOKEN_TYPE_PATH_SEP = "PATH_SEPARATOR"; + + public final static String TOKEN_TYPE_PATH_LENGTH = "PATH_LENGTH"; + + public final static String TOKEN_TYPE_PATH_ELEMENT_NAME = "PATH_ELEMENT_NAME"; + + public final static String TOKEN_TYPE_PATH_ELEMENT_NAMESPACE = "PATH_ELEMENT_NAMESPACE"; + + char pathSeparator; + + String separatorTokenText; + + String noNsTokenText; + + char nsStartDelimiter; + + int nsStartDelimiterLength; + + char nsEndDelimiter; + + int nsEndDelimiterLength; + + LinkedList tokens = new LinkedList(); + + Iterator it = null; + + private boolean includeNamespace; + + public PathTokenFilter(Reader in, char pathSeparator, String separatorTokenText, String noNsTokenText, + char nsStartDelimiter, char nsEndDelimiter, boolean includeNameSpace) + { + super(in); + this.pathSeparator = pathSeparator; + this.separatorTokenText = separatorTokenText; + this.noNsTokenText = noNsTokenText; + this.nsStartDelimiter = nsStartDelimiter; + this.nsEndDelimiter = nsEndDelimiter; + this.includeNamespace = includeNameSpace; + + this.nsStartDelimiterLength = 1; + this.nsEndDelimiterLength = 1; + + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.TokenStream#next() + */ + + public Token next() throws IOException + { + Token nextToken; + if (it == null) + { + buildTokenListAndIterator(); + } + if (it.hasNext()) + { + nextToken = it.next(); + } + else + { + nextToken = null; + } + return nextToken; + } + + private void buildTokenListAndIterator() throws IOException + { + NumberFormat nf = new DecimalFormat(INTEGER_FORMAT); + + // Could optimise to read each path ata time - not just all paths + int insertCountAt = 0; + int lengthCounter = 0; + Token t; + Token pathSplitToken = null; + Token nameToken = null; + Token countToken = null; + Token namespaceToken = null; + while ((t = nextToken()) != null) + { + String text = t.termText(); + + if((text.length() == 0) || text.equals(pathSeparator)) + { + break; + } + + if (text.charAt(text.length()-1) == pathSeparator) + { + text = text.substring(0, text.length() - 1); + pathSplitToken = new Token(separatorTokenText, t.startOffset(), t.endOffset(), TOKEN_TYPE_PATH_SEP); + pathSplitToken.setPositionIncrement(1); + + } + + int split = -1; + + if ((text.length() > 0) && (text.charAt(0) == nsStartDelimiter)) + { + split = text.indexOf(nsEndDelimiter); + } + if (split == -1) + { + namespaceToken = new Token(noNsTokenText, t.startOffset(), t.startOffset(), + TOKEN_TYPE_PATH_ELEMENT_NAMESPACE); + nameToken = new Token(text, t.startOffset(), t.endOffset(), TOKEN_TYPE_PATH_ELEMENT_NAME); + + } + else + { + namespaceToken = new Token(text.substring(nsStartDelimiterLength, (split + nsEndDelimiterLength - 1)), + t.startOffset(), t.startOffset() + split, TOKEN_TYPE_PATH_ELEMENT_NAMESPACE); + nameToken = new Token(text.substring(split + nsEndDelimiterLength), t.startOffset() + split + + nsEndDelimiterLength, t.endOffset(), TOKEN_TYPE_PATH_ELEMENT_NAME); + } + + namespaceToken.setPositionIncrement(1); + nameToken.setPositionIncrement(1); + + if (includeNamespace) + { + tokens.add(namespaceToken); + } + tokens.add(nameToken); + + lengthCounter++; + + if (pathSplitToken != null) + { + + String countString = nf.format(lengthCounter); + countToken = new Token(countString, t.startOffset(), t.endOffset(), TOKEN_TYPE_PATH_SEP); + countToken.setPositionIncrement(1); + + tokens.add(insertCountAt, countToken); + tokens.add(pathSplitToken); + + lengthCounter = 0; + insertCountAt = tokens.size(); + + pathSplitToken = null; + } + + } + + String countString = nf.format(lengthCounter); + countToken = new Token(countString, 0, 0, TOKEN_TYPE_PATH_SEP); + countToken.setPositionIncrement(1); + + tokens.add(insertCountAt, countToken); + + if ((tokens.size() == 0) || !(tokens.get(tokens.size() - 1).termText().equals(TOKEN_TYPE_PATH_SEP))) + { + pathSplitToken = new Token(separatorTokenText, 0, 0, TOKEN_TYPE_PATH_SEP); + pathSplitToken.setPositionIncrement(1); + tokens.add(pathSplitToken); + } + + it = tokens.iterator(); + } + + int readerPosition = 0; + + private Token nextToken() throws IOException + { + if(readerPosition == -1) + { + return null; + } + StringBuilder buffer = new StringBuilder(64); + boolean inNameSpace = false; + int start = readerPosition; + int current; + char c; + while((current = input.read()) != -1) + { + c = (char)current; + readerPosition++; + if(c == nsStartDelimiter) + { + inNameSpace = true; + } + else if(c == nsEndDelimiter) + { + inNameSpace = false; + } + else if(!inNameSpace && (c == '/')) + { + return new Token(buffer.toString(), start, readerPosition-1, "QNAME"); + } + buffer.append(c); + } + readerPosition = -1; + if(!inNameSpace) + { + return new Token(buffer.toString(), start, readerPosition-1, "QNAME"); + } + else + { + throw new IllegalStateException("QName terminated incorrectly: "+buffer.toString()); + } + + + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokeniser.java b/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokeniser.java new file mode 100644 index 0000000000..868baa3fb8 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/analysis/PathTokeniser.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.analysis; + +import java.io.Reader; + +import org.apache.lucene.analysis.CharTokenizer; + +/** + * @author andyh + * + * TODO To change the template for this generated type comment go to Window - + * Preferences - Java - Code Style - Code Templates + */ +public class PathTokeniser extends CharTokenizer +{ + public PathTokeniser(Reader in) + { + super(in); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.analysis.CharTokenizer#isTokenChar(char) + */ + protected boolean isTokenChar(char c) + { + return (c != '/') && !Character.isWhitespace(c); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerAware.java b/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerAware.java new file mode 100644 index 0000000000..9954551032 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerAware.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.fts; + +import org.alfresco.service.cmr.repository.StoreRef; + +public interface FTSIndexerAware +{ + + public void indexCompleted(StoreRef storeRef, int remaining, Exception e); +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerException.java b/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerException.java new file mode 100644 index 0000000000..fd7f646d53 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.fts; + +public class FTSIndexerException extends RuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = 3258134635127912754L; + + public FTSIndexerException() + { + super(); + } + + public FTSIndexerException(String message) + { + super(message); + } + + public FTSIndexerException(String message, Throwable cause) + { + super(message, cause); + } + + public FTSIndexerException(Throwable cause) + { + super(cause); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerJob.java b/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerJob.java new file mode 100644 index 0000000000..af60e229a1 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/fts/FTSIndexerJob.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.fts; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +public class FTSIndexerJob implements Job +{ + public FTSIndexerJob() + { + super(); + } + + public void execute(JobExecutionContext executionContext) throws JobExecutionException + { + + FullTextSearchIndexer indexer = (FullTextSearchIndexer)executionContext.getJobDetail().getJobDataMap().get("bean"); + if(indexer != null) + { + indexer.index(); + } + + } + + + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexer.java b/source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexer.java new file mode 100644 index 0000000000..99d2bd005e --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.fts; + +import org.alfresco.service.cmr.repository.StoreRef; + + + +public interface FullTextSearchIndexer { + + public abstract void requiresIndex(StoreRef storeRef); + + public abstract void indexCompleted(StoreRef storeRef, int remaining, Exception e); + + public abstract void pause() throws InterruptedException; + + public abstract void resume() throws InterruptedException; + + public abstract void index(); + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexerImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexerImpl.java new file mode 100644 index 0000000000..2763f517d9 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/fts/FullTextSearchIndexerImpl.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.fts; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.alfresco.repo.search.impl.lucene.LuceneIndexer; +import org.alfresco.repo.search.impl.lucene.LuceneIndexerAndSearcherFactory; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class FullTextSearchIndexerImpl implements FTSIndexerAware, FullTextSearchIndexer +{ + private enum State { + ACTIVE, PAUSING, PAUSED + }; + + private static Set requiresIndex = new LinkedHashSet(); + + private static Set indexing = new HashSet(); + + LuceneIndexerAndSearcherFactory luceneIndexerAndSearcherFactory; + + private int pauseCount = 0; + + private boolean paused = false; + + public FullTextSearchIndexerImpl() + { + super(); + //System.out.println("Created id is "+this); + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer#requiresIndex(org.alfresco.repo.ref.StoreRef) + */ + public synchronized void requiresIndex(StoreRef storeRef) + { + requiresIndex.add(storeRef); + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer#indexCompleted(org.alfresco.repo.ref.StoreRef, + * int, java.lang.Exception) + */ + public synchronized void indexCompleted(StoreRef storeRef, int remaining, Exception e) + { + try + { + indexing.remove(storeRef); + if ((remaining > 0) || (e != null)) + { + requiresIndex(storeRef); + } + if (e != null) + { + throw new FTSIndexerException(e); + } + } + finally + { + //System.out.println("..Index Complete: id is "+this); + this.notifyAll(); + } + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer#pause() + */ + public synchronized void pause() throws InterruptedException + { + pauseCount++; + //System.out.println("..Waiting "+pauseCount+" id is "+this); + while ((indexing.size() > 0)) + { + //System.out.println("Pause: Waiting with count of "+indexing.size()+" id is "+this); + this.wait(); + } + pauseCount--; + if(pauseCount == 0) + { + paused = true; + this.notifyAll(); // only resumers + } + //System.out.println("..Remaining "+pauseCount +" paused = "+paused+" id is "+this); + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer#resume() + */ + public synchronized void resume() throws InterruptedException + { + if(pauseCount == 0) + { + //System.out.println("Direct resume"+" id is "+this); + paused = false; + } + else + { + while(pauseCount > 0) + { + //System.out.println("Reusme waiting on "+pauseCount+" id is "+this); + this.wait(); + } + paused = false; + } + } + + private synchronized boolean isPaused() throws InterruptedException + { + if(pauseCount == 0) + { + return paused; + } + else + { + while(pauseCount > 0) + { + this.wait(); + } + return paused; + } + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer#index() + */ + public void index() + { + // Use the calling thread to index + // Parallel indexing via multiple Quartz thread initiating indexing + + StoreRef toIndex = getNextRef(); + if (toIndex != null) + { + //System.out.println("Indexing "+toIndex+" id is "+this); + LuceneIndexer indexer = luceneIndexerAndSearcherFactory.getIndexer(toIndex); + indexer.registerCallBack(this); + indexer.updateFullTextSearch(1000); + } + else + { + //System.out.println("Nothing to index"+" id is "+this); + } + } + + private synchronized StoreRef getNextRef() + { + if (paused || (pauseCount > 0)) + { + //System.out.println("Indexing suspended"+" id is "+this); + return null; + } + + StoreRef nextStoreRef = null; + + for (StoreRef ref : requiresIndex) + { + if (!indexing.contains(ref)) + { + nextStoreRef = ref; + } + } + + if (nextStoreRef != null) + { + requiresIndex.remove(nextStoreRef); + indexing.add(nextStoreRef); + } + + return nextStoreRef; + } + + public void setLuceneIndexerAndSearcherFactory(LuceneIndexerAndSearcherFactory luceneIndexerAndSearcherFactory) + { + this.luceneIndexerAndSearcherFactory = luceneIndexerAndSearcherFactory; + } + + public static void main(String[] args) throws InterruptedException + { + ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:alfresco/application-context.xml"); + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/AbsoluteStructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/AbsoluteStructuredFieldPosition.java new file mode 100644 index 0000000000..e83e63a514 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/AbsoluteStructuredFieldPosition.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +/** + * This class patches a term at a specified location. + * + * @author andyh + */ +public class AbsoluteStructuredFieldPosition extends AbstractStructuredFieldPosition +{ + + int requiredPosition; + + /** + * Search for a term at the specified position. + */ + + public AbsoluteStructuredFieldPosition(String termText, int position) + { + super(termText, true, true); + this.requiredPosition = position; + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.lucene.extensions.StructuredFieldPosition#matches(int, + * org.apache.lucene.index.TermPositions) + */ + public int matches(int start, int end, int offset) throws IOException + { + if (offset >= requiredPosition) + { + return -1; + } + + if (getCachingTermPositions() != null) + { + // Doing "termText" + getCachingTermPositions().reset(); + int count = getCachingTermPositions().freq(); + int realPosition = 0; + int adjustedPosition = 0; + for (int i = 0; i < count; i++) + { + realPosition = getCachingTermPositions().nextPosition(); + adjustedPosition = realPosition - start; + if ((end != -1) && (realPosition > end)) + { + return -1; + } + if (adjustedPosition > requiredPosition) + { + return -1; + } + if (adjustedPosition == requiredPosition) + { + return adjustedPosition; + } + + } + } + else + { + // Doing "*" + if ((offset + 1) == requiredPosition) + { + return offset + 1; + } + } + return -1; + + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.lucene.extensions.StructuredFieldPosition#getPosition() + */ + public int getPosition() + { + return requiredPosition; + } + + public String getDescription() + { + return "Absolute Named child"; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/AbstractStructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/AbstractStructuredFieldPosition.java new file mode 100644 index 0000000000..2bf1ce6038 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/AbstractStructuredFieldPosition.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +public abstract class AbstractStructuredFieldPosition implements StructuredFieldPosition +{ + private String termText; + + private boolean isTerminal; + + private boolean isAbsolute; + + private CachingTermPositions tps; + + public AbstractStructuredFieldPosition(String termText, boolean isTerminal, boolean isAbsolute) + { + super(); + this.termText = termText; + this.isTerminal = isTerminal; + this.isAbsolute = isAbsolute; + } + + public boolean isTerminal() + { + return isTerminal; + } + + protected void setTerminal(boolean isTerminal) + { + this.isTerminal = isTerminal; + } + + public boolean isAbsolute() + { + return isAbsolute; + } + + public boolean isRelative() + { + return !isAbsolute; + } + + public String getTermText() + { + return termText; + } + + public int getPosition() + { + return -1; + } + + public void setCachingTermPositions(CachingTermPositions tps) + { + this.tps = tps; + } + + public CachingTermPositions getCachingTermPositions() + { + return this.tps; + } + + + + public boolean allowsLinkingBySelf() + { + return false; + } + + public boolean allowslinkingByParent() + { + return true; + } + + public boolean linkParent() + { + return true; + } + + public boolean linkSelf() + { + return false; + } + + public String toString() + { + StringBuffer buffer = new StringBuffer(256); + buffer.append(getDescription()); + buffer.append("<"+getTermText()+"> at "+getPosition()); + buffer.append(" Terminal = "+isTerminal()); + buffer.append(" Absolute = "+isAbsolute()); + return buffer.toString(); + } + + public abstract String getDescription(); + + public boolean isDescendant() + { + return false; + } + + public boolean matchesAll() + { + return getCachingTermPositions() == null; + } + + + + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/AnyStructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/AnyStructuredFieldPosition.java new file mode 100644 index 0000000000..3af35eb30b --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/AnyStructuredFieldPosition.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +/** + * @author andyh + * + * TODO To change the template for this generated type comment go to Window - + * Preferences - Java - Code Style - Code Templates + */ +public class AnyStructuredFieldPosition extends AbstractStructuredFieldPosition +{ + + /** + * + */ + public AnyStructuredFieldPosition(String termText) + { + super(termText, true, false); + if (termText == null) + { + setTerminal(false); + } + } + + public AnyStructuredFieldPosition() + { + super(null, false, false); + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.lucene.extensions.StructuredFieldPosition#matches(int, + * int, org.apache.lucene.index.TermPositions) + */ + public int matches(int start, int end, int offset) throws IOException + { + // we are doing //name + if (getCachingTermPositions() != null) + { + setTerminal(true); + int realPosition = 0; + int adjustedPosition = 0; + getCachingTermPositions().reset(); + int count = getCachingTermPositions().freq(); + for (int i = 0; i < count; i++) + { + realPosition = getCachingTermPositions().nextPosition(); + adjustedPosition = realPosition - start; + if ((end != -1) && (realPosition > end)) + { + return -1; + } + if (adjustedPosition > offset) + { + return adjustedPosition; + } + } + } + else + { + // we are doing // + setTerminal(false); + return offset; + } + return -1; + } + + public String getDescription() + { + return "Any"; + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/CachingTermPositions.java b/source/java/org/alfresco/repo/search/impl/lucene/query/CachingTermPositions.java new file mode 100644 index 0000000000..671deb91bb --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/CachingTermPositions.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.index.TermPositions; + +/** + * @author andyh + * + * TODO To change the template for this generated type comment go to Window - + * Preferences - Java - Code Style - Code Templates + */ +public class CachingTermPositions implements TermPositions +{ + int[] results; + + int position = -1; + + int last = -1; + + TermPositions delegate; + + CachingTermPositions(TermPositions delegate) + { + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermPositions#nextPosition() + */ + public int nextPosition() throws IOException + { + if (results == null) + { + results = new int[freq()]; + } + position++; + if (last < position) + { + results[position] = delegate.nextPosition(); + last = position; + } + return results[position]; + + } + + public void reset() + { + position = -1; + } + + private void clear() + { + position = -1; + last = -1; + results = null; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#seek(org.apache.lucene.index.Term) + */ + public void seek(Term term) throws IOException + { + delegate.seek(term); + clear(); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#seek(org.apache.lucene.index.TermEnum) + */ + public void seek(TermEnum termEnum) throws IOException + { + delegate.seek(termEnum); + clear(); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#doc() + */ + public int doc() + { + return delegate.doc(); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#freq() + */ + public int freq() + { + return delegate.freq(); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#next() + */ + public boolean next() throws IOException + { + if (delegate.next()) + { + clear(); + return true; + } + else + { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#read(int[], int[]) + */ + public int read(int[] docs, int[] freqs) throws IOException + { + int answer = delegate.read(docs, freqs); + clear(); + return answer; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#skipTo(int) + */ + public boolean skipTo(int target) throws IOException + { + if (delegate.skipTo(target)) + { + clear(); + return true; + } + else + { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.index.TermDocs#close() + */ + public void close() throws IOException + { + delegate.close(); + clear(); + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/ContainerScorer.java b/source/java/org/alfresco/repo/search/impl/lucene/query/ContainerScorer.java new file mode 100644 index 0000000000..bd0ec0a7cc --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/ContainerScorer.java @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +import org.apache.lucene.index.TermPositions; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; + +/** + * The scorer for structured field queries. + * + * A document either matches or it does not, there for the frequency is reported + * as 0.0f or 1.0. + * + * + * + * @author andyh + */ +public class ContainerScorer extends Scorer +{ + // Unused + Weight weight; + + // Positions of documents with multiple structure elements + // e.g have mutiple paths, multiple categories or multiples entries in the + // same category + TermPositions root; + + // The Field positions that describe the structure we are trying to match + StructuredFieldPosition[] positions; + + // Unused at the moment + byte[] norms; + + // The minium document found so far + int min = 0; + + // The max document found so far + int max = 0; + + // The next root doc + // -1 and it has gone off the end + int rootDoc = 0; + + // Are there potentially more documents + boolean more = true; + + // The frequency of the terms in the doc (0.0f or 1.0f) + float freq = 0.0f; + + // A term position to find all container entries (there is no better way of finding the set of rquired containers) + private TermPositions containers; + + /** + * The arguments here follow the same pattern as used by the PhraseQuery. + * (It has the same unused arguments) + * + * @param weight - + * curently unsued + * @param tps - + * the term positions for the terms we are trying to find + * @param root - + * the term positions for documents with multiple entries - this + * may be null, or contain no matches - it specifies those things + * that appear under multiple categories etc. + * @param positions - + * the structured field positions - where terms should appear + * @param similarity - + * used in the abstract scorer implementation + * @param norms - + * unused + */ + public ContainerScorer(Weight weight, TermPositions root, StructuredFieldPosition[] positions, TermPositions containers, Similarity similarity, byte[] norms) + { + super(similarity); + this.weight = weight; + this.positions = positions; + this.norms = norms; + this.root = root; + this.containers = containers; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Scorer#next() + */ + public boolean next() throws IOException + { + // If there is no filtering + if (allContainers()) + { + // containers and roots must be in sync or the index is broken + while (more) + { + if (containers.next() && root.next()) + { + if (check(0, root.nextPosition())) + { + return true; + } + } + else + { + more = false; + return false; + } + } + } + + if (!more) + { + // One of the search terms has no more docuements + return false; + } + + if (max == 0) + { + // We need to initialise + // Just do a next on all terms and check if the first doc matches + doNextOnAll(); + if (found()) + { + return true; + } + // drop through to the normal find sequence + } + + return findNext(); + } + + /** + * Are we looking for all containers? + * If there are no positions we must have a better filter + * + * @return + */ + private boolean allContainers() + { + if (positions.length == 0) + { + return true; + } + for (StructuredFieldPosition sfp : positions) + { + if (sfp.getCachingTermPositions() != null) + { + return false; + } + } + return true; + } + + /** + * @return + * @throws IOException + */ + private boolean findNext() throws IOException + { + // Move to the next document + + while (more) + { + move(); // may set more to false + if (found()) + { + return true; + } + } + + // If we get here we must have no more documents + return false; + } + + /** + * Check if we have found a match + * + * @return + * @throws IOException + */ + + private boolean found() throws IOException + { + // No predicate test if there are no positions + if (positions.length == 0) + { + return true; + } + + // no more documents - no match + if (!more) + { + return false; + } + + // min and max must point to the same document + if (min != max) + { + return false; + } + + if (rootDoc != max) + { + return false; + } + + // We have duplicate entries - suport should be improved but it is not used at the moment + // This shuld work akin to the leaf scorer + // It would compact the index + // The match must be in a known term range + int count = root.freq(); + int start = 0; + int end = -1; + for (int i = 0; i < count; i++) + { + if (i == 0) + { + // First starts at zero + start = 0; + end = root.nextPosition() ; + } + else + { + start = end + 1; + end = root.nextPosition() ; + } + + if (check(start, end)) + { + return true; + } + } + + // We had checks to do and they all failed. + return false; + } + + /* + * We have all documents at the same state. Now we check the positions of + * the terms. + */ + + private boolean check(int start, int end) throws IOException + { + int offset = checkTail(start, end, 0, 0); + // Last match may fail + if (offset == -1) + { + return false; + } + else + { + // Check non // ending patterns end at the end of the available pattern + if (positions[positions.length - 1].isTerminal()) + { + return ((offset+1) == end); + } + else + { + return true; + } + } + } + + /** + * For // type pattern matches we need to test patterns of variable greedyness. + * + * + * @param start + * @param end + * @param currentPosition + * @param currentOffset + * @return + * @throws IOException + */ + private int checkTail(int start, int end, int currentPosition, int currentOffset) throws IOException + { + int offset = currentOffset; + for (int i = currentPosition, l = positions.length; i < l; i++) + { + offset = positions[i].matches(start, end, offset); + if (offset == -1) + { + return -1; + } + if (positions[i].isDescendant()) + { + for (int j = offset; j < end; j++) + { + int newOffset = checkTail(start, end, i + 1, j); + if (newOffset != -1) + { + return newOffset; + } + } + return -1; + } + } + return offset; + } + + /* + * Move to the next position to consider for a match test + */ + + private void move() throws IOException + { + if (min == max) + { + // If we were at a match just do next on all terms + // They all must move on + doNextOnAll(); + } + else + { + // We are in a range - try and skip to the max position on all terms + // Only some need to move on - some may move past the current max and set a new target + skipToMax(); + } + } + + /* + * Go through all the term positions and try and move to next document. Any + * failure measn we have no more. + * + * This can be used at initialisation and when moving away from an existing + * match. + * + * This will set min, max, more and rootDoc + * + */ + private void doNextOnAll() throws IOException + { + // Do the terms + int current; + boolean first = true; + for (int i = 0, l = positions.length; i < l; i++) + { + if (positions[i].getCachingTermPositions() != null) + { + if (positions[i].getCachingTermPositions().next()) + + { + current = positions[i].getCachingTermPositions().doc(); + adjustMinMax(current, first); + first = false; + } + else + { + more = false; + return; + } + } + } + + // Do the root term - it must always exists as the path could well have mutiple entries + // If an entry in the index does not have a root terminal it is broken + if (root.next()) + { + rootDoc = root.doc(); + } + else + { + more = false; + return; + } + if (root.doc() < max) + { + if (root.skipTo(max)) + { + rootDoc = root.doc(); + } + else + { + more = false; + return; + } + } + } + + /* + * Try and skip all those term positions at documents less than the current + * max up to value. This is quite likely to fail and leave us with (min != + * max) but that is OK, we try again. + * + * It is possible that max increases as we process terms, this is OK. We + * just failed to skip to a given value of max and start doing the next. + */ + private void skipToMax() throws IOException + { + // Do the terms + int current; + for (int i = 0, l = positions.length; i < l; i++) + { + if (i == 0) + { + min = max; + } + if (positions[i].getCachingTermPositions() != null) + { + if (positions[i].getCachingTermPositions().doc() < max) + { + if (positions[i].getCachingTermPositions().skipTo(max)) + { + current = positions[i].getCachingTermPositions().doc(); + adjustMinMax(current, false); + } + else + { + more = false; + return; + } + } + } + } + + // Do the root + if (root.doc() < max) + { + if (root.skipTo(max)) + { + rootDoc = root.doc(); + } + else + { + more = false; + return; + } + } + } + + /* + * Adjust the min and max values Convenience boolean to set or adjust the + * minimum. + */ + private void adjustMinMax(int doc, boolean setMin) + { + + if (max < doc) + { + max = doc; + } + + if (setMin) + { + min = doc; + } + else if (min > doc) + { + min = doc; + } + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Scorer#doc() + */ + public int doc() + { + if (allContainers()) + { + return containers.doc(); + } + return max; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Scorer#score() + */ + public float score() throws IOException + { + return 1.0f; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Scorer#skipTo(int) + */ + public boolean skipTo(int target) throws IOException + { + if (allContainers()) + { + containers.skipTo(target); + root.skipTo(containers.doc()); // must match + if (check(0, root.nextPosition())) + { + return true; + } + while (more) + { + if (containers.next() && root.next()) + { + if (check(0, root.nextPosition())) + { + return true; + } + } + else + { + more = false; + return false; + } + } + } + + max = target; + return findNext(); + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Scorer#explain(int) + */ + public Explanation explain(int doc) throws IOException + { + // TODO: Work out what a proper explanation would be here? + Explanation tfExplanation = new Explanation(); + + while (next() && doc() < doc) + { + } + + float phraseFreq = (doc() == doc) ? freq : 0.0f; + tfExplanation.setValue(getSimilarity().tf(phraseFreq)); + tfExplanation.setDescription("tf(phraseFreq=" + phraseFreq + ")"); + + return tfExplanation; + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/DeltaReader.java b/source/java/org/alfresco/repo/search/impl/lucene/query/DeltaReader.java new file mode 100644 index 0000000000..257590eb5a --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/DeltaReader.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; +import java.util.Arrays; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.index.TermPositions; + +public class DeltaReader extends MultiReader +{ + int[][] deletions; + + Boolean hasExclusions = null; + + private IndexReader[] subReaders; + + private int maxDoc = 0; + + private int[] starts; + + public DeltaReader(IndexReader[] readers, int[][] deletions) throws IOException + { + super(readers); + this.deletions = deletions; + initialize(readers); + } + + private void initialize(IndexReader[] subReaders) throws IOException + { + this.subReaders = subReaders; + starts = new int[subReaders.length + 1]; // build starts array + for (int i = 0; i < subReaders.length; i++) + { + starts[i] = maxDoc; + maxDoc += subReaders[i].maxDoc(); // compute maxDocs + } + starts[subReaders.length] = maxDoc; + } + + protected void doCommit() throws IOException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + protected void doDelete(int arg0) throws IOException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + protected void doUndeleteAll() throws IOException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public boolean hasDeletions() + { + return super.hasDeletions() || hasSearchExclusions(); + } + + private boolean hasSearchExclusions() + { + if (hasExclusions == null) + { + for (int i = 0; i < deletions.length; i++) + { + if (deletions[i].length > 0) + { + hasExclusions = new Boolean(true); + break; + } + } + hasExclusions = new Boolean(false); + } + return hasExclusions.booleanValue(); + } + + public boolean isDeleted(int docNumber) + { + int i = readerIndex(docNumber); + return super.isDeleted(docNumber) || (Arrays.binarySearch(deletions[i], docNumber - starts[i]) != -1); + } + + private int readerIndex(int n) + { // find reader for doc n: + int lo = 0; // search starts array + int hi = subReaders.length - 1; // for first element less + + while (hi >= lo) + { + int mid = (lo + hi) >> 1; + int midValue = starts[mid]; + if (n < midValue) + hi = mid - 1; + else if (n > midValue) + lo = mid + 1; + else + { // found a match + while (mid + 1 < subReaders.length && starts[mid + 1] == midValue) + { + mid++; // scan to last match + } + return mid; + } + } + return hi; + } + + public TermDocs termDocs() throws IOException + { + return new DeletingTermDocs(super.termDocs()); + } + + public TermPositions termPositions() throws IOException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + private class DeletingTermDocs implements TermDocs + { + TermDocs delegate; + + DeletingTermDocs(TermDocs delegate) + { + super(); + this.delegate = delegate; + } + + public void seek(Term term) throws IOException + { + delegate.seek(term); + } + + public void seek(TermEnum termEnum) throws IOException + { + delegate.seek(termEnum); + } + + public int doc() + { + return delegate.doc(); + } + + public int freq() + { + return delegate.freq(); + } + + public boolean next() throws IOException + { + while (delegate.next()) + { + if (!isDeleted(doc())) + { + return true; + } + } + return false; + } + + public int read(int[] docs, int[] freqs) throws IOException + { + int end; + int deletedCount; + do + { + end = delegate.read(docs, freqs); + if (end == 0) + { + return end; + } + deletedCount = 0; + for (int i = 0; i < end; i++) + { + if (!isDeleted(docs[i])) + { + deletedCount++; + } + } + } + while (end == deletedCount); + // fix up for deleted + int position = 0; + for(int i = 0; i < end; i++) + { + if(!isDeleted(i)) + { + docs[position] = docs[i]; + freqs[position] = freqs[i]; + position++; + } + } + return position; + } + + public boolean skipTo(int docNumber) throws IOException + { + delegate.skipTo(docNumber); + if (!isDeleted(doc())) + { + return true; + } + else + { + return next(); + } + } + + public void close() throws IOException + { + delegate.close(); + } + + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/DescendantAndSelfStructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/DescendantAndSelfStructuredFieldPosition.java new file mode 100644 index 0000000000..14eadc9aa1 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/DescendantAndSelfStructuredFieldPosition.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +public class DescendantAndSelfStructuredFieldPosition extends AnyStructuredFieldPosition +{ + public DescendantAndSelfStructuredFieldPosition() + { + super(); + } + + public String getDescription() + { + return "Descendant and Self Axis"; + } + + public boolean allowsLinkingBySelf() + { + return true; + } + + public boolean isDescendant() + { + return true; + } + + + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/LeafScorer.java b/source/java/org/alfresco/repo/search/impl/lucene/query/LeafScorer.java new file mode 100644 index 0000000000..fe05f1a671 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/LeafScorer.java @@ -0,0 +1,907 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.SearcherException; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.namespace.QName; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermPositions; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; + +public class LeafScorer extends Scorer +{ + static class Counter + { + int count = 0; + + public String toString() + { + return "count = " + count; + } + } + + private int counter; + + private int countInCounter; + + int min = 0; + + int max = 0; + + boolean more = true; + + Scorer containerScorer; + + StructuredFieldPosition[] sfps; + + float freq = 0.0f; + + HashMap parentIds = new HashMap(); + + HashMap> categories = new HashMap>(); + + HashMap selfIds = null; + + boolean hasSelfScorer; + + IndexReader reader; + + private TermPositions allNodes; + + TermPositions level0; + + HashSet selfLinks = new HashSet(); + + BitSet selfDocs = new BitSet(); + + private TermPositions root; + + private int rootDoc; + + private boolean repeat; + + private DictionaryService dictionaryService; + + private int[] parents; + + private int[] self; + + private int[] cats; + + private TermPositions tp; + + public LeafScorer(Weight weight, TermPositions root, TermPositions level0, ContainerScorer containerScorer, + StructuredFieldPosition[] sfps, TermPositions allNodes, HashMap selfIds, + IndexReader reader, Similarity similarity, byte[] norms, DictionaryService dictionaryService, + boolean repeat, TermPositions tp) + { + super(similarity); + this.root = root; + this.containerScorer = containerScorer; + this.sfps = sfps; + this.allNodes = allNodes; + this.tp = tp; + if (selfIds == null) + { + this.selfIds = new HashMap(); + hasSelfScorer = false; + } + else + { + this.selfIds = selfIds; + hasSelfScorer = true; + } + this.reader = reader; + this.level0 = level0; + this.dictionaryService = dictionaryService; + this.repeat = repeat; + try + { + initialise(); + } + catch (IOException e) + { + throw new SearcherException(e); + } + + } + + private void initialise() throws IOException + { + if (containerScorer != null) + { + parentIds.clear(); + while (containerScorer.next()) + { + int doc = containerScorer.doc(); + Document document = reader.document(doc); + Field id = document.getField("ID"); + Counter counter = parentIds.get(id.stringValue()); + if (counter == null) + { + counter = new Counter(); + parentIds.put(id.stringValue(), counter); + } + counter.count++; + + if (!hasSelfScorer) + { + counter = selfIds.get(id.stringValue()); + if (counter == null) + { + counter = new Counter(); + selfIds.put(id.stringValue(), counter); + } + counter.count++; + } + + Field isCategory = document.getField("ISCATEGORY"); + if (isCategory != null) + { + Field path = document.getField("PATH"); + String pathString = path.stringValue(); + if ((pathString.length() > 0) && (pathString.charAt(0) == '/')) + { + pathString = pathString.substring(1); + } + List list = categories.get(id.stringValue()); + if (list == null) + { + list = new ArrayList(); + categories.put(id.stringValue(), list); + } + list.add(pathString); + } + } + } + else if (level0 != null) + { + parentIds.clear(); + while (level0.next()) + { + int doc = level0.doc(); + Document document = reader.document(doc); + Field id = document.getField("ID"); + if (id != null) + { + Counter counter = parentIds.get(id.stringValue()); + if (counter == null) + { + counter = new Counter(); + parentIds.put(id.stringValue(), counter); + } + counter.count++; + + counter = selfIds.get(id.stringValue()); + if (counter == null) + { + counter = new Counter(); + selfIds.put(id.stringValue(), counter); + } + counter.count++; + } + } + if (parentIds.size() != 1) + { + throw new SearcherException("More than one root node? " + parentIds.size()); + } + } + + if (allNodes()) + { + int position = 0; + parents = new int[10000]; + for (String parent : parentIds.keySet()) + { + Counter counter = parentIds.get(parent); + tp.seek(new Term("PARENT", parent)); + while (tp.next()) + { + for (int i = 0, l = tp.freq(); i < l; i++) + { + for(int j = 0; j < counter.count; j++) + { + parents[position++] = tp.doc(); + if (position == parents.length) + { + int[] old = parents; + parents = new int[old.length * 2]; + System.arraycopy(old, 0, parents, 0, old.length); + } + } + + } + } + + } + int[] old = parents; + parents = new int[position]; + System.arraycopy(old, 0, parents, 0, position); + Arrays.sort(parents); + + position = 0; + self = new int[10000]; + for (String id : selfIds.keySet()) + { + tp.seek(new Term("ID", id)); + while (tp.next()) + { + Counter counter = selfIds.get(id); + for(int i = 0; i < counter.count; i++) + { + self[position++] = tp.doc(); + if (position == self.length) + { + old = self; + self = new int[old.length * 2]; + System.arraycopy(old, 0, self, 0, old.length); + } + } + } + + } + old = self; + self = new int[position]; + System.arraycopy(old, 0, self, 0, position); + Arrays.sort(self); + + position = 0; + cats = new int[10000]; + for (String catid : categories.keySet()) + { + for (QName apsectQName : dictionaryService.getAllAspects()) + { + AspectDefinition aspDef = dictionaryService.getAspect(apsectQName); + if (isCategorised(aspDef)) + { + for (PropertyDefinition propDef : aspDef.getProperties().values()) + { + if (propDef.getDataType().getName().equals(DataTypeDefinition.CATEGORY)) + { + tp.seek(new Term("@" + propDef.getName().toString(), catid)); + while (tp.next()) + { + for (int i = 0, l = tp.freq(); i < l; i++) + { + cats[position++] = tp.doc(); + if (position == cats.length) + { + old = cats; + cats = new int[old.length * 2]; + System.arraycopy(old, 0, cats, 0, old.length); + } + } + } + + } + } + } + } + + } + old = cats; + cats = new int[position]; + System.arraycopy(old, 0, cats, 0, position); + Arrays.sort(cats); + } + } + + public boolean next() throws IOException + { + + if (repeat && (countInCounter < counter)) + { + countInCounter++; + return true; + } + else + { + countInCounter = 1; + counter = 0; + } + + if (allNodes()) + { + while (more) + { + if (allNodes.next() && root.next()) + { + if (check()) + { + return true; + } + } + else + { + more = false; + return false; + } + } + } + + if (!more) + { + // One of the search terms has no more docuements + return false; + } + + if (max == 0) + { + // We need to initialise + // Just do a next on all terms and check if the first doc matches + doNextOnAll(); + if (found()) + { + return true; + } + } + + return findNext(); + } + + private boolean allNodes() + { + if (sfps.length == 0) + { + return true; + } + for (StructuredFieldPosition sfp : sfps) + { + if (sfp.getCachingTermPositions() != null) + { + return false; + } + } + return true; + } + + private boolean findNext() throws IOException + { + // Move to the next document + + while (more) + { + move(); // may set more to false + if (found()) + { + return true; + } + } + + // If we get here we must have no more documents + return false; + } + + private void skipToMax() throws IOException + { + // Do the terms + int current; + for (int i = 0, l = sfps.length; i < l; i++) + { + if (i == 0) + { + min = max; + } + if (sfps[i].getCachingTermPositions() != null) + { + if (sfps[i].getCachingTermPositions().doc() < max) + { + if (sfps[i].getCachingTermPositions().skipTo(max)) + { + current = sfps[i].getCachingTermPositions().doc(); + adjustMinMax(current, false); + } + else + { + more = false; + return; + } + } + } + } + + // Do the root + if (root.doc() < max) + { + if (root.skipTo(max)) + { + rootDoc = root.doc(); + } + else + { + more = false; + return; + } + } + } + + private void move() throws IOException + { + if (min == max) + { + // If we were at a match just do next on all terms + doNextOnAll(); + } + else + { + // We are in a range - try and skip to the max position on all terms + skipToMax(); + } + } + + private void doNextOnAll() throws IOException + { + // Do the terms + int current; + boolean first = true; + for (int i = 0, l = sfps.length; i < l; i++) + { + if (sfps[i].getCachingTermPositions() != null) + { + if (sfps[i].getCachingTermPositions().next()) + { + current = sfps[i].getCachingTermPositions().doc(); + adjustMinMax(current, first); + first = false; + } + else + { + more = false; + return; + } + } + } + + // Do the root term + if (root.next()) + { + rootDoc = root.doc(); + } + else + { + more = false; + return; + } + if (root.doc() < max) + { + if (root.skipTo(max)) + { + rootDoc = root.doc(); + } + else + { + more = false; + return; + } + } + } + + private void adjustMinMax(int doc, boolean setMin) + { + + if (max < doc) + { + max = doc; + } + + if (setMin) + { + min = doc; + } + else if (min > doc) + { + min = doc; + } + } + + private boolean found() throws IOException + { + if (sfps.length == 0) + { + return true; + } + + // no more documents - no match + if (!more) + { + return false; + } + + // min and max must point to the same document + if (min != max) + { + return false; + } + + if (rootDoc != max) + { + return false; + } + + return check(); + } + + private boolean check() throws IOException + { + if (allNodes()) + { + this.counter = 0; + int position; + + StructuredFieldPosition last = sfps[sfps.length - 1]; + + if (last.linkSelf()) + { + if ((self != null) && sfps[1].linkSelf() && ((position = Arrays.binarySearch(self, doc())) >= 0)) + { + if (!selfDocs.get(doc())) + { + selfDocs.set(doc()); + while (position > -1 && self[position] == doc()) + { + position--; + } + for (int i = position + 1, l = self.length; ((i < l) && (self[i] == doc())); i++) + { + this.counter++; + } + } + } + } + if (!selfDocs.get(doc()) && last.linkParent()) + { + if ((parents != null) && ((position = Arrays.binarySearch(parents, doc())) >= 0)) + { + while (position > -1 && parents[position] == doc()) + { + position--; + } + for (int i = position + 1, l = parents.length; ((i < l) && (parents[i] == doc())); i++) + { + this.counter++; + } + } + + if ((cats != null) && ((position = Arrays.binarySearch(cats, doc())) >= 0)) + { + while (position > -1 && cats[position] == doc()) + { + position--; + } + for (int i = position + 1, l = cats.length; ((i < l) && (cats[i] == doc())); i++) + { + this.counter++; + } + } + } + return counter > 0; + } + + // String name = reader.document(doc()).getField("QNAME").stringValue(); + // We have duplicate entries + // The match must be in a known term range + int count = root.freq(); + int start = 0; + int end = -1; + for (int i = 0; i < count; i++) + { + if (i == 0) + { + // First starts at zero + start = 0; + end = root.nextPosition(); + } + else + { + start = end + 1; + end = root.nextPosition(); + } + + check(start, end, i); + + } + // We had checks to do and they all failed. + return this.counter > 0; + } + + private void check(int start, int end, int position) throws IOException + { + int offset = 0; + for (int i = 0, l = sfps.length; i < l; i++) + { + offset = sfps[i].matches(start, end, offset); + if (offset == -1) + { + return; + } + } + // Last match may fail + if (offset == -1) + { + return; + } + else + { + if ((sfps[sfps.length - 1].isTerminal()) && (offset != 2)) + { + return; + } + } + + Document doc = reader.document(doc()); + Field[] parentFields = doc.getFields("PARENT"); + Field[] linkFields = doc.getFields("LINKASPECT"); + + String parentID = null; + String linkAspect = null; + if ((parentFields != null) && (parentFields.length > position) && (parentFields[position] != null)) + { + parentID = parentFields[position].stringValue(); + } + if ((linkFields != null) && (linkFields.length > position) && (linkFields[position] != null)) + { + linkAspect = linkFields[position].stringValue(); + } + + containersIncludeCurrent(doc, parentID, linkAspect); + + } + + private void containersIncludeCurrent(Document document, String parentID, String aspectQName) throws IOException + { + if ((containerScorer != null) || (level0 != null)) + { + if (sfps.length == 0) + { + return; + } + String id = document.getField("ID").stringValue(); + StructuredFieldPosition last = sfps[sfps.length - 1]; + if ((last.linkSelf() && selfIds.containsKey(id))) + { + Counter counter = selfIds.get(id); + if (counter != null) + { + if (!selfLinks.contains(id)) + { + this.counter += counter.count; + selfLinks.add(id); + return; + } + } + } + if ((parentID != null) && (parentID.length() > 0) && last.linkParent()) + { + if (!selfLinks.contains(id)) + { + if (categories.containsKey(parentID)) + { + Field typeField = document.getField("TYPE"); + if ((typeField != null) && (typeField.stringValue() != null)) + { + QName typeRef = QName.createQName(typeField.stringValue()); + if (isCategory(typeRef)) + { + Counter counter = parentIds.get(parentID); + if (counter != null) + { + this.counter += counter.count; + return; + } + } + } + + if (aspectQName != null) + { + QName classRef = QName.createQName(aspectQName); + AspectDefinition aspDef = dictionaryService.getAspect(classRef); + if (isCategorised(aspDef)) + { + for (PropertyDefinition propDef : aspDef.getProperties().values()) + { + if (propDef.getDataType().getName().equals(DataTypeDefinition.CATEGORY)) + { + // get field and compare to ID + // Check in path as QName + // somewhere + Field[] categoryFields = document.getFields("@" + propDef.getName()); + if (categoryFields != null) + { + for (Field categoryField : categoryFields) + { + if ((categoryField != null) && (categoryField.stringValue() != null)) + { + if (categoryField.stringValue().endsWith(parentID)) + { + int count = 0; + List paths = categories.get(parentID); + if (paths != null) + { + for (String path : paths) + { + if (path.indexOf(aspectQName) != -1) + { + count++; + } + } + } + this.counter += count; + return; + } + } + } + } + } + } + } + + } + } + else + { + Counter counter = parentIds.get(parentID); + if (counter != null) + { + this.counter += counter.count; + return; + } + } + + } + } + + return; + } + else + { + return; + } + } + + private boolean isCategory(QName classRef) + { + if (classRef == null) + { + return false; + } + TypeDefinition current = dictionaryService.getType(classRef); + while (current != null) + { + if (current.getName().equals(ContentModel.TYPE_CATEGORY)) + { + return true; + } + else + { + QName parentName = current.getParentName(); + if (parentName == null) + { + break; + } + current = dictionaryService.getType(parentName); + } + } + return false; + } + + private boolean isCategorised(AspectDefinition aspDef) + { + AspectDefinition current = aspDef; + while (current != null) + { + if (current.getName().equals(ContentModel.ASPECT_CLASSIFIABLE)) + { + return true; + } + else + { + QName parentName = current.getParentName(); + if (parentName == null) + { + break; + } + current = dictionaryService.getAspect(parentName); + } + } + return false; + } + + public int doc() + { + if (allNodes()) + { + return allNodes.doc(); + } + return max; + } + + public float score() throws IOException + { + return repeat ? 1.0f : counter; + } + + public boolean skipTo(int target) throws IOException + { + + countInCounter = 1; + counter = 0; + + if (allNodes()) + { + allNodes.skipTo(target); + root.skipTo(allNodes.doc()); // must match + if (check()) + { + return true; + } + while (more) + { + if (allNodes.next() && root.next()) + { + if (check()) + { + return true; + } + } + else + { + more = false; + return false; + } + } + } + + max = target; + return findNext(); + } + + public Explanation explain(int doc) throws IOException + { + Explanation tfExplanation = new Explanation(); + + while (next() && doc() < doc) + { + } + + float phraseFreq = (doc() == doc) ? freq : 0.0f; + tfExplanation.setValue(getSimilarity().tf(phraseFreq)); + tfExplanation.setDescription("tf(phraseFreq=" + phraseFreq + ")"); + + return tfExplanation; + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/PathQuery.java b/source/java/org/alfresco/repo/search/impl/lucene/query/PathQuery.java new file mode 100644 index 0000000000..3ec86a8898 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/PathQuery.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Weight; + +/** + * An extension to the Lucene query set. + * + * This query supports structured queries against paths. + * + * The field must have been tokenised using the path tokeniser. + * + * This class manages linking together an ordered chain of absolute and relative + * positional queries. + * + * @author Andy Hind + */ +public class PathQuery extends Query +{ + /** + * + */ + private static final long serialVersionUID = 3832904355660707892L; + + private String pathField = "PATH"; + + private String qNameField = "QNAME"; + + private int unitSize = 2; + + private List pathStructuredFieldPositions = new ArrayList(); + + private List qNameStructuredFieldPositions = new ArrayList(); + + private DictionaryService dictionarySertvice; + + private boolean repeats = false; + + /** + * The base query + * + * @param query + */ + + public PathQuery(DictionaryService dictionarySertvice) + { + super(); + this.dictionarySertvice = dictionarySertvice; + } + + public void setQuery(List path, List qname) + { + qNameStructuredFieldPositions.clear(); + pathStructuredFieldPositions.clear(); + if (qname.size() != unitSize) + { + throw new UnsupportedOperationException(); + } + if (path.size() % unitSize != 0) + { + throw new UnsupportedOperationException(); + } + qNameStructuredFieldPositions.addAll(qname); + pathStructuredFieldPositions.addAll(path); + } + + public void appendQuery(List sfps) + { + if (sfps.size() != unitSize) + { + throw new UnsupportedOperationException(); + } + + StructuredFieldPosition last = null; + StructuredFieldPosition next = sfps.get(0); + + if (qNameStructuredFieldPositions.size() > 0) + { + last = qNameStructuredFieldPositions.get(qNameStructuredFieldPositions.size() - 1); + } + + if ((last != null) && next.linkParent() && !last.allowslinkingByParent()) + { + return; + } + + if ((last != null) && next.linkSelf() && !last.allowsLinkingBySelf()) + { + return; + } + + if (qNameStructuredFieldPositions.size() == unitSize) + { + pathStructuredFieldPositions.addAll(qNameStructuredFieldPositions); + } + qNameStructuredFieldPositions.clear(); + qNameStructuredFieldPositions.addAll(sfps); + } + + public String getPathField() + { + return pathField; + } + + public void setPathField(String pathField) + { + this.pathField = pathField; + } + + public String getQnameField() + { + return qNameField; + } + + public void setQnameField(String qnameField) + { + this.qNameField = qnameField; + } + + public Term getPathRootTerm() + { + return new Term(getPathField(), ";"); + } + + public Term getQNameRootTerm() + { + return new Term(getQnameField(), ";"); + } + + /* + * @see org.apache.lucene.search.Query#createWeight(org.apache.lucene.search.Searcher) + */ + protected Weight createWeight(Searcher searcher) + { + return new StructuredFieldWeight(searcher); + } + + /* + * @see java.lang.Object#toString() + */ + public String toString() + { + return ""; + } + + /* + * @see org.apache.lucene.search.Query#toString(java.lang.String) + */ + public String toString(String field) + { + return ""; + } + + private class StructuredFieldWeight implements Weight + { + + /** + * + */ + private static final long serialVersionUID = 3257854259645985328L; + + private Searcher searcher; + + private float value; + + private float idf; + + private float queryNorm; + + private float queryWeight; + + public StructuredFieldWeight(Searcher searcher) + { + this.searcher = searcher; + + } + + /* + * @see org.apache.lucene.search.Weight#explain(org.apache.lucene.index.IndexReader, + * int) + */ + public Explanation explain(IndexReader reader, int doc) throws IOException + { + throw new UnsupportedOperationException(); + } + + /* + * @see org.apache.lucene.search.Weight#getQuery() + */ + public Query getQuery() + { + return PathQuery.this; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Weight#getValue() + */ + public float getValue() + { + return value; + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Weight#normalize(float) + */ + public void normalize(float queryNorm) + { + this.queryNorm = queryNorm; + queryWeight *= queryNorm; // normalize query weight + value = queryWeight * idf; // idf for document + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Weight#scorer(org.apache.lucene.index.IndexReader) + */ + public Scorer scorer(IndexReader reader) throws IOException + { + return PathScorer.createPathScorer(getSimilarity(searcher), PathQuery.this, reader, this, dictionarySertvice, repeats); + + } + + /* + * (non-Javadoc) + * + * @see org.apache.lucene.search.Weight#sumOfSquaredWeights() + */ + public float sumOfSquaredWeights() throws IOException + { + idf = getSimilarity(searcher).idf(getTerms(), searcher); // compute + // idf + queryWeight = idf * getBoost(); // compute query weight + return queryWeight * queryWeight; // square it + } + + private ArrayList getTerms() + { + ArrayList answer = new ArrayList(pathStructuredFieldPositions.size()); + for (StructuredFieldPosition sfp : pathStructuredFieldPositions) + { + if (sfp.getTermText() != null) + { + Term term = new Term(pathField, sfp.getTermText()); + answer.add(term); + } + } + return answer; + } + } + + public void removeDescendantAndSelf() + { + while ((getLast() != null) && getLast().linkSelf()) + { + removeLast(); + removeLast(); + } + } + + private StructuredFieldPosition getLast() + + { + if (qNameStructuredFieldPositions.size() > 0) + { + return qNameStructuredFieldPositions.get(qNameStructuredFieldPositions.size() - 1); + } + else + { + return null; + } + } + + private void removeLast() + { + qNameStructuredFieldPositions.clear(); + for (int i = 0; i < unitSize; i++) + { + if (pathStructuredFieldPositions.size() > 0) + { + qNameStructuredFieldPositions.add(0, pathStructuredFieldPositions.remove(pathStructuredFieldPositions.size() - 1)); + } + } + } + + public boolean isEmpty() + { + return qNameStructuredFieldPositions.size() == 0; + } + + public List getPathStructuredFieldPositions() + { + return pathStructuredFieldPositions; + } + + + public List getQNameStructuredFieldPositions() + { + return qNameStructuredFieldPositions; + } + + public void setRepeats(boolean repeats) + { + this.repeats = repeats; + } + + + + + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/PathScorer.java b/source/java/org/alfresco/repo/search/impl/lucene/query/PathScorer.java new file mode 100644 index 0000000000..ea644ce124 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/PathScorer.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; +import java.util.HashMap; + +import org.alfresco.repo.search.impl.lucene.query.LeafScorer.Counter; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermPositions; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; + +public class PathScorer extends Scorer +{ + Scorer scorer; + + PathScorer(Similarity similarity, Scorer scorer) + { + super(similarity); + this.scorer = scorer; + } + + + public static PathScorer createPathScorer(Similarity similarity, PathQuery pathQuery, IndexReader reader, Weight weight, DictionaryService dictionarySertvice, boolean repeat) throws IOException + { + Scorer selfScorer = null; + HashMap selfIds = null; + + StructuredFieldPosition last = null; + if(pathQuery.getQNameStructuredFieldPositions().size() > 0) + { + last = pathQuery.getQNameStructuredFieldPositions().get(pathQuery.getQNameStructuredFieldPositions().size() - 1); + } + if ((last != null) && last.linkSelf()) + { + PathQuery selfQuery = new PathQuery(dictionarySertvice); + selfQuery.setQuery(pathQuery.getPathStructuredFieldPositions(), pathQuery.getQNameStructuredFieldPositions()); + selfQuery.removeDescendantAndSelf(); + if (!selfQuery.isEmpty()) + { + selfIds = new HashMap(); + selfScorer = PathScorer.createPathScorer(similarity, selfQuery, reader, weight, dictionarySertvice, repeat); + selfIds.clear(); + while (selfScorer.next()) + { + int doc = selfScorer.doc(); + Document document = reader.document(doc); + Field id = document.getField("ID"); + Counter counter = selfIds.get(id.stringValue()); + if (counter == null) + { + counter = new Counter(); + selfIds.put(id.stringValue(), counter); + } + counter.count++; + } + } + } + + + if ((pathQuery.getPathStructuredFieldPositions().size() + pathQuery.getQNameStructuredFieldPositions().size()) == 0) // optimize + // zero-term + // case + return null; + + + for (StructuredFieldPosition sfp : pathQuery.getPathStructuredFieldPositions()) + { + if (sfp.getTermText() != null) + { + TermPositions p = reader.termPositions(new Term(pathQuery.getPathField(), sfp.getTermText())); + if (p == null) + return null; + CachingTermPositions ctp = new CachingTermPositions(p); + sfp.setCachingTermPositions(ctp); + } + } + + for (StructuredFieldPosition sfp : pathQuery.getQNameStructuredFieldPositions()) + { + if (sfp.getTermText() != null) + { + TermPositions p = reader.termPositions(new Term(pathQuery.getQnameField(), sfp.getTermText())); + if (p == null) + return null; + CachingTermPositions ctp = new CachingTermPositions(p); + sfp.setCachingTermPositions(ctp); + } + } + + TermPositions rootContainerPositions = null; + if (pathQuery.getPathRootTerm() != null) + { + rootContainerPositions = reader.termPositions(pathQuery.getPathRootTerm()); + } + + TermPositions rootLeafPositions = null; + if (pathQuery.getQNameRootTerm() != null) + { + rootLeafPositions = reader.termPositions(pathQuery.getQNameRootTerm()); + } + + + TermPositions tp = reader.termPositions(); + + ContainerScorer cs = null; + + TermPositions level0 = null; + + TermPositions nodePositions = reader.termPositions(new Term("ISNODE", "T")); + + // StructuredFieldPosition[] test = + // (StructuredFieldPosition[])structuredFieldPositions.toArray(new + // StructuredFieldPosition[]{}); + if (pathQuery.getPathStructuredFieldPositions().size() > 0) + { + TermPositions containerPositions = reader.termPositions(new Term("ISCONTAINER", "T")); + cs = new ContainerScorer(weight, rootContainerPositions, (StructuredFieldPosition[]) pathQuery.getPathStructuredFieldPositions().toArray(new StructuredFieldPosition[] {}), + containerPositions, similarity, reader.norms(pathQuery.getPathField())); + } + else + { + level0 = reader.termPositions(new Term("ISROOT", "T")); + } + + LeafScorer ls = new LeafScorer(weight, rootLeafPositions, level0, cs, (StructuredFieldPosition[]) pathQuery.getQNameStructuredFieldPositions().toArray(new StructuredFieldPosition[] {}), nodePositions, + selfIds, reader, similarity, reader.norms(pathQuery.getQnameField()), dictionarySertvice, repeat, tp); + + return new PathScorer(similarity, ls); + } + + @Override + public boolean next() throws IOException + { + return scorer.next(); + } + + @Override + public int doc() + { + return scorer.doc(); + } + + @Override + public float score() throws IOException + { + return scorer.score(); + } + + @Override + public boolean skipTo(int position) throws IOException + { + return scorer.skipTo(position); + } + + @Override + public Explanation explain(int position) throws IOException + { + return scorer.explain(position); + } + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/RelativeStructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/RelativeStructuredFieldPosition.java new file mode 100644 index 0000000000..efd349df59 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/RelativeStructuredFieldPosition.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +/** + * Search for a term relative to the last one found. + * + * @author andyh + */ +public class RelativeStructuredFieldPosition extends AbstractStructuredFieldPosition +{ + + int relativePosition; + + /** + * + */ + public RelativeStructuredFieldPosition(String termText) + { + super(termText.equals("*") ? null : termText, true, false); + relativePosition = 1; + + } + + public RelativeStructuredFieldPosition() + { + super(null, false, false); + relativePosition = 1; + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.lucene.extensions.StructuredFieldPosition#matches(int, + * int, org.apache.lucene.index.TermPositions) + */ + public int matches(int start, int end, int offset) throws IOException + { + + if (getCachingTermPositions() != null) + { + // Doing "termText" + getCachingTermPositions().reset(); + int count = getCachingTermPositions().freq(); + int requiredPosition = offset + relativePosition; + int realPosition = 0; + int adjustedPosition = 0; + for (int i = 0; i < count; i++) + { + realPosition = getCachingTermPositions().nextPosition(); + adjustedPosition = realPosition - start; + if ((end != -1) && (realPosition > end)) + { + return -1; + } + if (adjustedPosition == requiredPosition) + { + return adjustedPosition; + } + if (adjustedPosition > requiredPosition) + { + return -1; + } + } + } + else + { + // Doing "*"; + return offset + 1; + } + return -1; + } + + public String getDescription() + { + return "Relative Named child"; + } +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/SelfAxisStructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/SelfAxisStructuredFieldPosition.java new file mode 100644 index 0000000000..058fc8887a --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/SelfAxisStructuredFieldPosition.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +public class SelfAxisStructuredFieldPosition extends AbstractStructuredFieldPosition +{ + + public SelfAxisStructuredFieldPosition() + { + super(null, true, false); + } + + public int matches(int start, int end, int offset) throws IOException + { + return offset; + } + + public String getDescription() + { + return "Self Axis"; + } + + public boolean linkSelf() + { + return true; + } + + public boolean isTerminal() + { + return false; + } + + + + +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldPosition.java b/source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldPosition.java new file mode 100644 index 0000000000..917d1c9193 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldPosition.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import java.io.IOException; + +/** + * Elements used to test agains path and Qname + * + * @author andyh + */ +public interface StructuredFieldPosition +{ + + /** + * Does this element match + * + * @param start - + * the start postion of the paths terms + * @param end - + * the end position of the paths terms + * @param offset - + * the current offset in the path + * @return returns the next match position (usually offset + 1) or -1 if it + * does not match. + * @throws IOException + */ + public int matches(int start, int end, int offset) throws IOException; + + /** + * If this position is last in the chain and it is terminal it will ensure + * it is an exact match for the length of the chain found. If false, it will + * effectively allow prefix mathces for the likes of descendant-and-below + * style queries. + * + * @return + */ + public boolean isTerminal(); + + /** + * Is this an absolute element; that is, it knows its exact position. + * + * @return + */ + public boolean isAbsolute(); + + /** + * This element only knows its position relative to the previous element. + * + * @return + */ + public boolean isRelative(); + + /** + * Get the test to search for in the term query. This may be null if it + * should not have a term query + * + * @return + */ + public String getTermText(); + + /** + * If absolute return the position. If relative we could compute the + * position knowing the previous term unless this element is preceded by a + * descendat and below style element + * + * @return + */ + public int getPosition(); + + /** + * A reference to the caching term positions this element uses. This may be + * null which indicates all terms match, in that case there is no action + * against the index + * + * @param tps + */ + public void setCachingTermPositions(CachingTermPositions tps); + + public CachingTermPositions getCachingTermPositions(); + + /** + * Normally paths would require onlt parent chaining. for some it is parent + * and child chaining. + * + * @return + */ + + public boolean linkSelf(); + + public boolean linkParent(); + + public boolean allowslinkingByParent(); + + public boolean allowsLinkingBySelf(); + + public boolean isDescendant(); + + public boolean matchesAll(); +} diff --git a/source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldTerm.java b/source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldTerm.java new file mode 100644 index 0000000000..f27de761e4 --- /dev/null +++ b/source/java/org/alfresco/repo/search/impl/lucene/query/StructuredFieldTerm.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.impl.lucene.query; + +import org.apache.lucene.index.Term; + +/** + * @author andyh + */ +public class StructuredFieldTerm +{ + + private Term term; + + private StructuredFieldPosition sfp; + + /** + * + */ + public StructuredFieldTerm(Term term, StructuredFieldPosition sfp) + { + this.term = term; + this.sfp = sfp; + } + + /** + * @return Returns the sfp. + */ + public StructuredFieldPosition getSfp() + { + return sfp; + } + + /** + * @return Returns the term. + */ + public Term getTerm() + { + return term; + } +} diff --git a/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSet.java b/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSet.java new file mode 100644 index 0000000000..6eb0526c9e --- /dev/null +++ b/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSet.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +/* + * Created on 07-Jun-2005 + * + * TODO Comment this class + * + * + */ +package org.alfresco.repo.search.results; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.alfresco.repo.search.AbstractResultSet; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSetRow; + +public class ChildAssocRefResultSet extends AbstractResultSet +{ + private List cars; + NodeService nodeService; + + public ChildAssocRefResultSet(NodeService nodeService, List cars, Path[] propertyPaths) + { + super(propertyPaths); + this.nodeService = nodeService; + this.cars = cars; + } + + public ChildAssocRefResultSet(NodeService nodeService, List nodeRefs, Path[] propertyPaths, boolean resolveAllParents) + { + super(propertyPaths); + this.nodeService = nodeService; + List cars = new ArrayList(nodeRefs.size()); + for(NodeRef nodeRef : nodeRefs) + { + if(resolveAllParents) + { + cars.addAll(nodeService.getParentAssocs(nodeRef)); + } + else + { + cars.add(nodeService.getPrimaryParent(nodeRef)); + } + } + this.cars = cars; + } + + public int length() + { + return cars.size(); + } + + public NodeRef getNodeRef(int n) + { + return cars.get(n).getChildRef(); + } + + public ChildAssociationRef getChildAssocRef(int n) + { + return cars.get(n); + } + + public ResultSetRow getRow(int i) + { + return new ChildAssocRefResultSetRow(this, i); + } + + public Iterator iterator() + { + return new ChildAssocRefResultSetRowIterator(this); + } + + public NodeService getNodeService() + { + return nodeService; + } + +} diff --git a/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRow.java b/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRow.java new file mode 100644 index 0000000000..2a22bb1811 --- /dev/null +++ b/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRow.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.results; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.search.AbstractResultSetRow; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; + +public class ChildAssocRefResultSetRow extends AbstractResultSetRow +{ + public ChildAssocRefResultSetRow(ChildAssocRefResultSet resultSet, int index) + { + super(resultSet, index); + } + + public Serializable getValue(Path path) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public QName getQName() + { + return ((ChildAssocRefResultSet)getResultSet()).getChildAssocRef(getIndex()).getQName(); + } + + @Override + protected Map getDirectProperties() + { + return ((ChildAssocRefResultSet)getResultSet()).getNodeService().getProperties(getNodeRef()); + } + + public ChildAssociationRef getChildAssocRef() + { + return ((ChildAssocRefResultSet)getResultSet()).getChildAssocRef(getIndex()); + } + +} diff --git a/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRowIterator.java b/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRowIterator.java new file mode 100644 index 0000000000..e8b46bb50c --- /dev/null +++ b/source/java/org/alfresco/repo/search/results/ChildAssocRefResultSetRowIterator.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.results; + +import org.alfresco.repo.search.AbstractResultSetRowIterator; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; + +public class ChildAssocRefResultSetRowIterator extends AbstractResultSetRowIterator +{ + + public ChildAssocRefResultSetRowIterator(ResultSet resultSet) + { + super(resultSet); + } + + @Override + public ResultSetRow next() + { + return new ChildAssocRefResultSetRow((ChildAssocRefResultSet)getResultSet(), moveToNextPosition()); + } + + @Override + public ResultSetRow previous() + { + return new ChildAssocRefResultSetRow((ChildAssocRefResultSet)getResultSet(), moveToPreviousPosition()); + } + +} diff --git a/source/java/org/alfresco/repo/search/results/DetachedResultSet.java b/source/java/org/alfresco/repo/search/results/DetachedResultSet.java new file mode 100644 index 0000000000..2443040d21 --- /dev/null +++ b/source/java/org/alfresco/repo/search/results/DetachedResultSet.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.results; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.alfresco.repo.search.AbstractResultSet; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; + +public class DetachedResultSet extends AbstractResultSet +{ + List rows = null; + + public DetachedResultSet(ResultSet resultSet, Path[] propertyPaths) + { + super(propertyPaths); + rows = new ArrayList(resultSet.length()); + for (ResultSetRow row : resultSet) + { + rows.add(new DetachedResultSetRow(this, row)); + } + } + + public int length() + { + return rows.size(); + } + + public NodeRef getNodeRef(int n) + { + return rows.get(n).getNodeRef(); + } + + public ResultSetRow getRow(int i) + { + return rows.get(i); + } + + public Iterator iterator() + { + return rows.iterator(); + } + + public ChildAssociationRef getChildAssocRef(int n) + { + return rows.get(n).getChildAssocRef(); + } + +} diff --git a/source/java/org/alfresco/repo/search/results/DetachedResultSetRow.java b/source/java/org/alfresco/repo/search/results/DetachedResultSetRow.java new file mode 100644 index 0000000000..3c45bc896e --- /dev/null +++ b/source/java/org/alfresco/repo/search/results/DetachedResultSetRow.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.results; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.search.AbstractResultSetRow; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.namespace.QName; + +public class DetachedResultSetRow extends AbstractResultSetRow +{ + private ChildAssociationRef car; + private Map properties; + + public DetachedResultSetRow(ResultSet resultSet, ResultSetRow row) + { + super(resultSet, row.getIndex()); + car = row.getChildAssocRef(); + properties = row.getValues(); + } + + public Serializable getValue(Path path) + { + return properties.get(path); + } + + public QName getQName() + { + return car.getQName(); + } + + public NodeRef getNodeRef() + { + return car.getChildRef(); + } + + public Map getValues() + { + return properties; + } + + public ChildAssociationRef getChildAssocRef() + { + return car; + } + + + +} diff --git a/source/java/org/alfresco/repo/search/transaction/LuceneIndexLock.java b/source/java/org/alfresco/repo/search/transaction/LuceneIndexLock.java new file mode 100644 index 0000000000..faa5f2c98e --- /dev/null +++ b/source/java/org/alfresco/repo/search/transaction/LuceneIndexLock.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.transaction; + +import java.util.HashMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.alfresco.service.cmr.repository.StoreRef; + +public class LuceneIndexLock +{ + private HashMap locks = new HashMap (); + + public LuceneIndexLock() + { + super(); + } + + public void getReadLock(StoreRef ref) + { + return; + } + + public void releaseReadLock(StoreRef ref) + { + return; + } + + public void getWriteLock(StoreRef ref) + { + ReentrantLock lock; + synchronized(locks) + { + lock = locks.get(ref); + if(lock == null) + { + lock = new ReentrantLock(true); + locks.put(ref, lock); + } + } + lock.lock(); + } + + public void releaseWriteLock(StoreRef ref) + { + ReentrantLock lock; + synchronized(locks) + { + lock = locks.get(ref); + } + if(lock != null) + { + lock.unlock(); + } + + } +} diff --git a/source/java/org/alfresco/repo/search/transaction/LuceneTransactionException.java b/source/java/org/alfresco/repo/search/transaction/LuceneTransactionException.java new file mode 100644 index 0000000000..507b68fb43 --- /dev/null +++ b/source/java/org/alfresco/repo/search/transaction/LuceneTransactionException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.transaction; + +import org.springframework.transaction.TransactionException; + +/** + * @author Andy Hind + */ +public class LuceneTransactionException extends TransactionException +{ + private static final long serialVersionUID = 3978985453464335925L; + + public LuceneTransactionException(String arg0) + { + super(arg0); + } + + public LuceneTransactionException(String arg0, Throwable arg1) + { + super(arg0, arg1); + } +} diff --git a/source/java/org/alfresco/repo/search/transaction/SimpleTransaction.java b/source/java/org/alfresco/repo/search/transaction/SimpleTransaction.java new file mode 100644 index 0000000000..76df29e75e --- /dev/null +++ b/source/java/org/alfresco/repo/search/transaction/SimpleTransaction.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.transaction; + +import java.io.UnsupportedEncodingException; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.xa.XAResource; + +import org.alfresco.util.GUID; + +public class SimpleTransaction implements XidTransaction +{ + private static final int DEFAULT_TIMEOUT = 600; + + private boolean isRollBackOnly; + + private int timeout; + + public static final int FORMAT_ID = 12321; + + private static final String CHAR_SET = "UTF-8"; + + private byte[] globalTransactionId; + + private byte[] branchQualifier; + + // This is the transactoin id + private String guid; + + private static ThreadLocal transaction = new ThreadLocal(); + + private SimpleTransaction(int timeout) + { + super(); + this.timeout = timeout; + guid = GUID.generate(); + try + { + globalTransactionId = guid.getBytes(CHAR_SET); + } + catch (UnsupportedEncodingException e) + { + throw new XidException(e); + } + branchQualifier = new byte[0]; + } + + private SimpleTransaction() + { + this(DEFAULT_TIMEOUT); + } + + public static SimpleTransaction getTransaction() + { + return (SimpleTransaction) transaction.get(); + } + + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, + SecurityException, SystemException + { + try + { + if (isRollBackOnly) + { + throw new RollbackException("Commit failed: Transaction marked for rollback"); + } + + } + finally + { + transaction.set(null); + } + } + + public boolean delistResource(XAResource arg0, int arg1) throws IllegalStateException, SystemException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public boolean enlistResource(XAResource arg0) throws RollbackException, IllegalStateException, SystemException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public int getStatus() throws SystemException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void registerSynchronization(Synchronization arg0) throws RollbackException, IllegalStateException, + SystemException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void rollback() throws IllegalStateException, SystemException + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void setRollbackOnly() throws IllegalStateException, SystemException + { + isRollBackOnly = true; + } + + /* + * Support for suspend, resume and begin. + */ + + /* package */static SimpleTransaction suspend() + { + SimpleTransaction tx = getTransaction(); + transaction.set(null); + return tx; + } + + /* package */static void begin() throws NotSupportedException + { + if (getTransaction() != null) + { + throw new NotSupportedException("Nested transactions are not supported"); + } + transaction.set(new SimpleTransaction()); + } + + /* package */static void resume(SimpleTransaction tx) + { + if (getTransaction() != null) + { + throw new IllegalStateException("A transaction is already associated with the thread"); + } + transaction.set((SimpleTransaction) tx); + } + + public String getGUID() + { + return guid; + } + + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof SimpleTransaction)) + { + return false; + } + SimpleTransaction other = (SimpleTransaction) o; + return this.getGUID().equals(other.getGUID()); + } + + public int hashCode() + { + return getGUID().hashCode(); + } + + public String toString() + { + StringBuffer buffer = new StringBuffer(128); + buffer.append("Simple Transaction GUID = " + getGUID()); + return buffer.toString(); + } + + public int getFormatId() + { + return FORMAT_ID; + } + + public byte[] getGlobalTransactionId() + { + return globalTransactionId; + } + + public byte[] getBranchQualifier() + { + return branchQualifier; + } +} diff --git a/source/java/org/alfresco/repo/search/transaction/SimpleTransactionManager.java b/source/java/org/alfresco/repo/search/transaction/SimpleTransactionManager.java new file mode 100644 index 0000000000..8f4868cc08 --- /dev/null +++ b/source/java/org/alfresco/repo/search/transaction/SimpleTransactionManager.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.transaction; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +public class SimpleTransactionManager implements TransactionManager +{ + private static SimpleTransactionManager manager = new SimpleTransactionManager(); + + private int timeout; + + private SimpleTransactionManager() + { + super(); + } + + public static SimpleTransactionManager getInstance() + { + return manager; + } + + public void begin() throws NotSupportedException, SystemException + { + SimpleTransaction.begin(); + + } + + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, + SecurityException, IllegalStateException, SystemException + { + SimpleTransaction transaction = getTransactionChecked(); + transaction.commit(); + } + + public int getStatus() throws SystemException + { + SimpleTransaction transaction = getTransactionChecked(); + return transaction.getStatus(); + } + + public SimpleTransaction getTransaction() throws SystemException + { + return SimpleTransaction.getTransaction(); + } + + private SimpleTransaction getTransactionChecked() throws SystemException, IllegalStateException + { + SimpleTransaction tx = SimpleTransaction.getTransaction(); + if (tx == null) + { + throw new IllegalStateException("The thread is not bound to a transaction."); + } + return tx; + } + + public void resume(Transaction tx) throws InvalidTransactionException, IllegalStateException, SystemException + { + if (!(tx instanceof SimpleTransaction)) + { + throw new IllegalStateException("Transaction must be a SimpleTransaction to resume"); + } + SimpleTransaction.resume((SimpleTransaction) tx); + } + + public void rollback() throws IllegalStateException, SecurityException, SystemException + { + SimpleTransaction transaction = getTransactionChecked(); + transaction.rollback(); + } + + public void setRollbackOnly() throws IllegalStateException, SystemException + { + SimpleTransaction transaction = getTransactionChecked(); + transaction.setRollbackOnly(); + } + + public void setTransactionTimeout(int timeout) throws SystemException + { + this.timeout = timeout; + } + + public SimpleTransaction suspend() throws SystemException + { + return SimpleTransaction.suspend(); + } + +} diff --git a/source/java/org/alfresco/repo/search/transaction/XidException.java b/source/java/org/alfresco/repo/search/transaction/XidException.java new file mode 100644 index 0000000000..a089c2354d --- /dev/null +++ b/source/java/org/alfresco/repo/search/transaction/XidException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.transaction; + +public class XidException extends RuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = 3257847696969840185L; + + public XidException() + { + super(); + } + + public XidException(String message) + { + super(message); + } + + public XidException(String message, Throwable cause) + { + super(message, cause); + } + + public XidException(Throwable cause) + { + super(cause); + } + +} diff --git a/source/java/org/alfresco/repo/search/transaction/XidTransaction.java b/source/java/org/alfresco/repo/search/transaction/XidTransaction.java new file mode 100644 index 0000000000..ea2efdd292 --- /dev/null +++ b/source/java/org/alfresco/repo/search/transaction/XidTransaction.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.search.transaction; + +import javax.transaction.Transaction; +import javax.transaction.xa.Xid; + +public interface XidTransaction extends Xid, Transaction +{ + +} diff --git a/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java b/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java new file mode 100644 index 0000000000..ee2bf18d48 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import org.alfresco.error.AlfrescoRuntimeException; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.context.Context; +import net.sf.acegisecurity.context.ContextHolder; +import net.sf.acegisecurity.context.security.SecureContext; +import net.sf.acegisecurity.context.security.SecureContextImpl; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.User; + +/** + * This class abstract the support required to set up and query the Acegi + * context for security enforcement. + * + * There are some simple default method implementations to support simple + * authentication. + * + * @author Andy Hind + */ +public abstract class AbstractAuthenticationComponent implements AuthenticationComponent +{ + + // Name of the system user + + private static final String SYSTEM_USER_NAME = "System"; + + public AbstractAuthenticationComponent() + { + super(); + } + + /** + * Explicitly set the current user to be authenticated. + * + * @param userName + * String + * @return Authentication + */ + public Authentication setCurrentUser(String userName) + { + try + { + UserDetails ud = null; + if (userName.equals(SYSTEM_USER_NAME)) + { + GrantedAuthority[] gas = new GrantedAuthority[1]; + gas[0] = new GrantedAuthorityImpl("ROLE_SYSTEM"); + ud = new User(SYSTEM_USER_NAME, "", true, true, true, true, gas); + } + else + { + ud = getUserDetails(userName); + } + + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(ud, "", ud + .getAuthorities()); + auth.setDetails(ud); + auth.setAuthenticated(true); + return setCurrentAuthentication(auth); + } + catch (net.sf.acegisecurity.AuthenticationException ae) + { + throw new AuthenticationException(ae.getMessage(), ae); + } + } + + /** + * Default implementation that makes an ACEGI object on the fly + * + * @param userName + * @return + */ + protected UserDetails getUserDetails(String userName) + { + GrantedAuthority[] gas = new GrantedAuthority[1]; + gas[0] = new GrantedAuthorityImpl("ROLE_AUTHENTICATED"); + UserDetails ud = new User(userName, "", true, true, true, true, gas); + return ud; + } + + /** + * Explicitly set the current authentication. + * + * @param authentication + * Authentication + */ + public Authentication setCurrentAuthentication(Authentication authentication) + { + Context context = ContextHolder.getContext(); + SecureContext sc = null; + if ((context == null) || !(context instanceof SecureContext)) + { + sc = new SecureContextImpl(); + ContextHolder.setContext(sc); + } + else + { + sc = (SecureContext) context; + } + authentication.setAuthenticated(true); + sc.setAuthentication(authentication); + return authentication; + } + + /** + * Get the current authentication context + * + * @return Authentication + * @throws AuthenticationException + */ + public Authentication getCurrentAuthentication() throws AuthenticationException + { + Context context = ContextHolder.getContext(); + if ((context == null) || !(context instanceof SecureContext)) + { + return null; + } + return ((SecureContext) context).getAuthentication(); + } + + /** + * Get the current user name. + * + * @return String + * @throws AuthenticationException + */ + public String getCurrentUserName() throws AuthenticationException + { + Context context = ContextHolder.getContext(); + if ((context == null) || !(context instanceof SecureContext)) + { + return null; + } + return getUserName(((SecureContext) context).getAuthentication()); + } + + /** + * Get the current user name + * + * @param authentication + * Authentication + * @return String + */ + private String getUserName(Authentication authentication) + { + String username = authentication.getPrincipal().toString(); + + if (authentication.getPrincipal() instanceof UserDetails) + { + username = ((UserDetails) authentication.getPrincipal()).getUsername(); + } + + return username; + } + + /** + * Set the system user as the current user. + * + * @return Authentication + */ + public Authentication setSystemUserAsCurrentUser() + { + return setCurrentUser(SYSTEM_USER_NAME); + } + + /** + * Get the name of the system user + * + * @return String + */ + public String getSystemUserName() + { + return SYSTEM_USER_NAME; + } + + /** + * Remove the current security information + */ + public void clearCurrentSecurityContext() + { + ContextHolder.setContext(null); + } + + /** + * The default is not to support Authentication token base authentication + */ + public Authentication authenticate(Authentication token) throws AuthenticationException + { + throw new AlfrescoRuntimeException("Authentication via token not supported"); + } + + /** + * The should only be supported if getNTLMMode() is NTLMMode.MD4_PROVIDER. + */ + public String getMD4HashedPassword(String userName) + { + throw new UnsupportedOperationException(); + } + + /** + * Get the NTML mode - none - supports MD4 hash to integrate - or it can + * asct as an NTLM authentication + */ + public NTLMMode getNTLMMode() + { + return NTLMMode.NONE; + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticatedAuthenticationPassthroughProvider.java b/source/java/org/alfresco/repo/security/authentication/AuthenticatedAuthenticationPassthroughProvider.java new file mode 100644 index 0000000000..47e080f6ee --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticatedAuthenticationPassthroughProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.providers.AuthenticationProvider; + +public class AuthenticatedAuthenticationPassthroughProvider implements AuthenticationProvider +{ + + public AuthenticatedAuthenticationPassthroughProvider() + { + super(); + } + + public Authentication authenticate(Authentication authentication) throws AuthenticationException + { + if (!supports(authentication.getClass())) { + return null; + } + if(authentication.isAuthenticated()) + { + return authentication; + } + else + { + return null; + } + } + + public boolean supports(Class authentication) + { + return (Authentication.class.isAssignableFrom(authentication)); + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationComponent.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationComponent.java new file mode 100644 index 0000000000..e811146373 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationComponent.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import net.sf.acegisecurity.Authentication; + +public interface AuthenticationComponent +{ + + /** + * Authenticate + * + * @param userName + * @param password + * @throws AuthenticationException + */ + public void authenticate(String userName, char[] password) throws AuthenticationException; + + /** + * Authenticate using a token + * + * @param token Authentication + * @return Authentication + * @throws AuthenticationException + */ + public Authentication authenticate(Authentication token) throws AuthenticationException; + + /** + * Explicitly set the current user to be authenticated. + */ + + public Authentication setCurrentUser(String userName); + + /** + * Remove the current security information + * + */ + public void clearCurrentSecurityContext(); + + /** + * Explicitly set the current suthentication. + */ + + public Authentication setCurrentAuthentication(Authentication authentication); + + /** + * + * @return + * @throws AuthenticationException + */ + public Authentication getCurrentAuthentication() throws AuthenticationException; + + /** + * Set the system user as the current user. + * + * @return + */ + public Authentication setSystemUserAsCurrentUser(); + + + /** + * Get the name of the system user + * + * @return + */ + public String getSystemUserName(); + + /** + * Get the current user name. + * + * @return + * @throws AuthenticationException + */ + public String getCurrentUserName() throws AuthenticationException; + + /** + * Get the enum that describes NTLM integration + * + * @return + */ + public NTLMMode getNTLMMode(); + + /** + * Get the MD4 password hash, as required by NTLM based authentication methods. + * + * @param userName + * @return + */ + public String getMD4HashedPassword(String userName); +} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java new file mode 100644 index 0000000000..46d86f9bc5 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import net.sf.acegisecurity.AuthenticationManager; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; + +public class AuthenticationComponentImpl extends AbstractAuthenticationComponent +{ + private MutableAuthenticationDao authenticationDao; + + AuthenticationManager authenticationManager; + + public AuthenticationComponentImpl() + { + super(); + } + + /** + * IOC + * + * @param authenticationManager + */ + public void setAuthenticationManager(AuthenticationManager authenticationManager) + { + this.authenticationManager = authenticationManager; + } + + /** + * IOC + * + * @param authenticationDao + */ + public void setAuthenticationDao(MutableAuthenticationDao authenticationDao) + { + this.authenticationDao = authenticationDao; + } + + /** + * Authenticate + */ + public void authenticate(String userName, char[] password) throws AuthenticationException + { + try + { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userName, + new String(password)); + authenticationManager.authenticate(authentication); + setCurrentUser(userName); + + } + catch (net.sf.acegisecurity.AuthenticationException ae) + { + throw new AuthenticationException(ae.getMessage(), ae); + } + } + + + /** + * We actually have an acegi object so override the default method. + */ + protected UserDetails getUserDetails(String userName) + { + return (UserDetails) authenticationDao.loadUserByUsername(userName); + } + + + /** + * Get the password hash from the DAO + */ + public String getMD4HashedPassword(String userName) + { + return authenticationDao.getMD4HashedPassword(userName); + } + + + /** + * This implementation supported MD4 password hashes. + */ + public NTLMMode getNTLMMode() + { + return NTLMMode.MD4_PROVIDER; + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationException.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationException.java new file mode 100644 index 0000000000..f9e0dea995 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Alfresco Authentication Exception and wrapper + * + * @author andyh + * + */ +public class AuthenticationException extends AlfrescoRuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = 3546647620128092466L; + + public AuthenticationException(String msg) + { + super(msg); + } + + public AuthenticationException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java new file mode 100644 index 0000000000..84d2551e77 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import org.alfresco.repo.security.permissions.PermissionServiceSPI; +import org.alfresco.service.cmr.security.AuthenticationService; + +public class AuthenticationServiceImpl implements AuthenticationService +{ + MutableAuthenticationDao authenticationDao; + + AuthenticationComponent authenticationComponent; + + TicketComponent ticketComponent; + + PermissionServiceSPI permissionServiceSPI; + + public AuthenticationServiceImpl() + { + super(); + } + + public void setAuthenticationDao(MutableAuthenticationDao authenticationDao) + { + this.authenticationDao = authenticationDao; + } + + public void setTicketComponent(TicketComponent ticketComponent) + { + this.ticketComponent = ticketComponent; + } + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + public void setPermissionServiceSPI(PermissionServiceSPI permissionServiceSPI) + { + this.permissionServiceSPI = permissionServiceSPI; + } + + public void createAuthentication(String userName, char[] password) throws AuthenticationException + { + authenticationDao.createUser(userName, password); + } + + public void updateAuthentication(String userName, char[] oldPassword, char[] newPassword) + throws AuthenticationException + { + authenticationDao.updateUser(userName, newPassword); + } + + public void setAuthentication(String userName, char[] newPassword) throws AuthenticationException + { + authenticationDao.updateUser(userName, newPassword); + } + + public void deleteAuthentication(String userName) throws AuthenticationException + { + authenticationDao.deleteUser(userName); + permissionServiceSPI.deletePermissions(authenticationDao.getUserNamesAreCaseSensitive() ? userName: userName.toLowerCase()); + } + + public boolean getAuthenticationEnabled(String userName) throws AuthenticationException + { + return authenticationDao.getEnabled(userName); + } + + public void setAuthenticationEnabled(String userName, boolean enabled) throws AuthenticationException + { + authenticationDao.setEnabled(userName, enabled); + } + + public void authenticate(String userName, char[] password) throws AuthenticationException + { + authenticationComponent.authenticate(userName, password); + } + + public String getCurrentUserName() throws AuthenticationException + { + return authenticationComponent.getCurrentUserName(); + } + + public void invalidateUserSession(String userName) throws AuthenticationException + { + ticketComponent.invalidateTicketByUser(userName); + } + + public void invalidateTicket(String ticket) throws AuthenticationException + { + ticketComponent.invalidateTicketById(ticket); + } + + public void validate(String ticket) throws AuthenticationException + { + authenticationComponent.setCurrentUser(ticketComponent.validateTicket(ticket)); + } + + public String getCurrentTicket() + { + return ticketComponent.getTicket(getCurrentUserName()); + } + + public void clearCurrentSecurityContext() + { + authenticationComponent.clearCurrentSecurityContext(); + } + + public boolean isCurrentUserTheSystemUser() + { + String userName = getCurrentUserName(); + if ((userName != null) && userName.equals(authenticationComponent.getSystemUserName())) + { + return true; + } + return false; + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java new file mode 100644 index 0000000000..36b6fbf0e7 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java @@ -0,0 +1,711 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; +import net.sf.acegisecurity.AccountExpiredException; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationManager; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.CredentialsExpiredException; +import net.sf.acegisecurity.DisabledException; +import net.sf.acegisecurity.LockedException; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.SaltSource; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.permissions.PermissionServiceSPI; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class AuthenticationTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private NodeService nodeService; + + private SearchService searchService; + + private NodeRef rootNodeRef; + + private NodeRef systemNodeRef; + + private NodeRef typesNodeRef; + + private NodeRef personAndyNodeRef; + + private DictionaryService dictionaryService; + + private MD4PasswordEncoder passwordEncoder; + + private MutableAuthenticationDao dao; + + private AuthenticationManager authenticationManager; + + private SaltSource saltSource; + + private TicketComponent ticketComponent; + + private AuthenticationService authenticationService; + + private AuthenticationService pubAuthenticationService; + + private AuthenticationComponent authenticationComponent; + + private PermissionServiceSPI permissionServiceSPI; + + private UserTransaction userTransaction; + + public AuthenticationTest() + { + super(); + } + + public AuthenticationTest(String arg0) + { + super(arg0); + } + + public void setUp() throws Exception + { + + nodeService = (NodeService) ctx.getBean("nodeService"); + searchService = (SearchService) ctx.getBean("searchService"); + dictionaryService = (DictionaryService) ctx.getBean("dictionaryService"); + passwordEncoder = (MD4PasswordEncoder) ctx.getBean("passwordEncoder"); + ticketComponent = (TicketComponent) ctx.getBean("ticketComponent"); + authenticationService = (AuthenticationService) ctx.getBean("authenticationService"); + pubAuthenticationService = (AuthenticationService) ctx.getBean("AuthenticationService"); + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + permissionServiceSPI = (PermissionServiceSPI) ctx.getBean("permissionService"); + + + dao = (MutableAuthenticationDao) ctx.getBean("alfDaoImpl"); + authenticationManager = (AuthenticationManager) ctx.getBean("authenticationManager"); + saltSource = (SaltSource) ctx.getBean("saltSource"); + + TransactionService transactionService = (TransactionService) ctx.getBean(ServiceRegistry.TRANSACTION_SERVICE + .getLocalName()); + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + QName children = ContentModel.ASSOC_CHILDREN; + QName system = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "system"); + QName container = ContentModel.TYPE_CONTAINER; + QName types = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "people"); + + systemNodeRef = nodeService.createNode(rootNodeRef, children, system, container).getChildRef(); + typesNodeRef = nodeService.createNode(systemNodeRef, children, types, container).getChildRef(); + Map props = createPersonProperties("Andy"); + personAndyNodeRef = nodeService.createNode(typesNodeRef, children, ContentModel.TYPE_PERSON, container, props) + .getChildRef(); + assertNotNull(personAndyNodeRef); + + deleteAndy(); + + } + + private void deleteAndy() + { + RepositoryAuthenticationDao dao = new RepositoryAuthenticationDao(); + dao.setNodeService(nodeService); + dao.setSearchService(searchService); + dao.setDictionaryService(dictionaryService); + dao.setNamespaceService(getNamespacePrefixReolsver("")); + dao.setPasswordEncoder(passwordEncoder); + + if(dao.getUserOrNull("andy") != null) + { + dao.deleteUser("andy"); + } + } + + @Override + protected void tearDown() throws Exception + { + userTransaction.rollback(); + super.tearDown(); + } + + private Map createPersonProperties(String userName) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, "Andy"); + return properties; + } + + public void testCreateAndyUserAndOtherCRUD() throws NoSuchAlgorithmException, UnsupportedEncodingException + { + RepositoryAuthenticationDao dao = new RepositoryAuthenticationDao(); + dao.setNodeService(nodeService); + dao.setSearchService(searchService); + dao.setDictionaryService(dictionaryService); + dao.setNamespaceService(getNamespacePrefixReolsver("")); + dao.setPasswordEncoder(passwordEncoder); + + dao.createUser("Andy", "cabbage".toCharArray()); + assertNotNull(dao.getUserOrNull("Andy")); + + byte[] decodedHash = passwordEncoder.decodeHash(dao.getMD4HashedPassword("Andy")); + byte[] testHash = MessageDigest.getInstance("MD4").digest("cabbage".getBytes("UnicodeLittleUnmarked")); + assertEquals(new String(decodedHash), new String(testHash)); + + UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy"); + assertNotNull(AndyDetails); + assertEquals(dao.getUserNamesAreCaseSensitive() ? "Andy" : "andy", AndyDetails.getUsername()); + // assertNotNull(dao.getSalt(AndyDetails)); + assertTrue(AndyDetails.isAccountNonExpired()); + assertTrue(AndyDetails.isAccountNonLocked()); + assertTrue(AndyDetails.isCredentialsNonExpired()); + assertTrue(AndyDetails.isEnabled()); + assertNotSame("cabbage", AndyDetails.getPassword()); + assertEquals(AndyDetails.getPassword(), passwordEncoder.encodePassword("cabbage", saltSource + .getSalt(AndyDetails))); + assertEquals(1, AndyDetails.getAuthorities().length); + + // Object oldSalt = dao.getSalt(AndyDetails); + dao.updateUser("Andy", "carrot".toCharArray()); + UserDetails newDetails = (UserDetails) dao.loadUserByUsername("Andy"); + assertNotNull(newDetails); + assertEquals(dao.getUserNamesAreCaseSensitive() ? "Andy" : "andy", newDetails.getUsername()); + // assertNotNull(dao.getSalt(newDetails)); + assertTrue(newDetails.isAccountNonExpired()); + assertTrue(newDetails.isAccountNonLocked()); + assertTrue(newDetails.isCredentialsNonExpired()); + assertTrue(newDetails.isEnabled()); + assertNotSame("carrot", newDetails.getPassword()); + assertEquals(1, newDetails.getAuthorities().length); + + assertNotSame(AndyDetails.getPassword(), newDetails.getPassword()); + // assertNotSame(oldSalt, dao.getSalt(newDetails)); + + dao.deleteUser("Andy"); + assertNull(dao.getUserOrNull("Andy")); + + MessageDigest digester; + try + { + digester = MessageDigest.getInstance("MD4"); + System.out.println("Digester from " + digester.getProvider()); + } + catch (NoSuchAlgorithmException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + System.out.println("No digester"); + } + + } + + public void testAuthentication() + { + dao.createUser("GUEST", "".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("GUEST", ""); + token.setAuthenticated(false); + + Authentication result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.createUser("Andy", "squash".toCharArray()); + + token = new UsernamePasswordAuthenticationToken("Andy", "squash"); + token.setAuthenticated(false); + + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setEnabled("Andy", false); + try + { + result = authenticationManager.authenticate(token); + assertNotNull(result); + assertNotNull(null); + } + catch (DisabledException e) + { + // Expected + } + + dao.setEnabled("Andy", true); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setLocked("Andy", true); + try + { + result = authenticationManager.authenticate(token); + assertNotNull(result); + assertNotNull(null); + } + catch (LockedException e) + { + // Expected + } + + dao.setLocked("Andy", false); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setAccountExpires("Andy", true); + dao.setCredentialsExpire("Andy", true); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setAccountExpiryDate("Andy", null); + dao.setCredentialsExpiryDate("Andy", null); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setAccountExpiryDate("Andy", new Date(new Date().getTime() + 10000)); + dao.setCredentialsExpiryDate("Andy", new Date(new Date().getTime() + 10000)); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setAccountExpiryDate("Andy", new Date(new Date().getTime() - 10000)); + try + { + result = authenticationManager.authenticate(token); + assertNotNull(result); + assertNotNull(null); + } + catch (AccountExpiredException e) + { + // Expected + } + dao.setAccountExpiryDate("Andy", new Date(new Date().getTime() + 10000)); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.setCredentialsExpiryDate("Andy", new Date(new Date().getTime() - 10000)); + try + { + result = authenticationManager.authenticate(token); + assertNotNull(result); + assertNotNull(null); + } + catch (CredentialsExpiredException e) + { + // Expected + } + dao.setCredentialsExpiryDate("Andy", new Date(new Date().getTime() + 10000)); + result = authenticationManager.authenticate(token); + assertNotNull(result); + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + public void testAuthenticationFailure() + { + dao.createUser("Andy", "squash".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Andy", "turnip"); + token.setAuthenticated(false); + + try + { + Authentication result = authenticationManager.authenticate(token); + assertNotNull(result); + assertNotNull(null); + } + catch (BadCredentialsException e) + { + // Expected + } + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + public void testTicket() + { + dao.createUser("Andy", "ticket".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Andy", "ticket"); + token.setAuthenticated(false); + + Authentication result = authenticationManager.authenticate(token); + result.setAuthenticated(true); + + String ticket = ticketComponent.getTicket(getUserName(result)); + String user = ticketComponent.validateTicket(ticket); + + user = null; + try + { + user = ticketComponent.validateTicket("INVALID"); + assertNotNull(null); + } + catch (AuthenticationException e) + { + assertNull(user); + } + + ticketComponent.invalidateTicketById(ticket); + try + { + user = ticketComponent.validateTicket(ticket); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + + } + + public void testTicketRepeat() + { + InMemoryTicketComponentImpl tc = new InMemoryTicketComponentImpl(); + tc.setOneOff(false); + tc.setTicketsExpire(false); + tc.setValidDuration("P0D"); + + dao.createUser("Andy", "ticket".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Andy", "ticket"); + token.setAuthenticated(false); + + Authentication result = authenticationManager.authenticate(token); + result.setAuthenticated(true); + + String ticket = tc.getTicket(getUserName(result)); + tc.validateTicket(ticket); + tc.validateTicket(ticket); + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + public void testTicketOneOff() + { + InMemoryTicketComponentImpl tc = new InMemoryTicketComponentImpl(); + tc.setOneOff(true); + tc.setTicketsExpire(false); + tc.setValidDuration("P0D"); + + dao.createUser("Andy", "ticket".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Andy", "ticket"); + token.setAuthenticated(false); + + Authentication result = authenticationManager.authenticate(token); + result.setAuthenticated(true); + + String ticket = tc.getTicket(getUserName(result)); + tc.validateTicket(ticket); + try + { + tc.validateTicket(ticket); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + public void testTicketExpires() + { + InMemoryTicketComponentImpl tc = new InMemoryTicketComponentImpl(); + tc.setOneOff(false); + tc.setTicketsExpire(true); + tc.setValidDuration("P5S"); + + dao.createUser("Andy", "ticket".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Andy", "ticket"); + token.setAuthenticated(false); + + Authentication result = authenticationManager.authenticate(token); + result.setAuthenticated(true); + + String ticket = tc.getTicket(getUserName(result)); + tc.validateTicket(ticket); + tc.validateTicket(ticket); + tc.validateTicket(ticket); + synchronized (this) + { + try + { + wait(10000); + } + catch (InterruptedException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + try + { + tc.validateTicket(ticket); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + public void testTicketDoesNotExpire() + { + InMemoryTicketComponentImpl tc = new InMemoryTicketComponentImpl(); + tc.setOneOff(false); + tc.setTicketsExpire(true); + tc.setValidDuration("P1D"); + + dao.createUser("Andy", "ticket".toCharArray()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Andy", "ticket"); + token.setAuthenticated(false); + + Authentication result = authenticationManager.authenticate(token); + result.setAuthenticated(true); + + String ticket = tc.getTicket(getUserName(result)); + tc.validateTicket(ticket); + tc.validateTicket(ticket); + tc.validateTicket(ticket); + synchronized (this) + { + try + { + wait(10000); + } + catch (InterruptedException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + tc.validateTicket(ticket); + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + + } + + public void testAuthenticationService() + { + authenticationService.createAuthentication("GUEST", "".toCharArray()); + authenticationService.authenticate("GUEST", "".toCharArray()); + + // create an authentication object e.g. the user + authenticationService.createAuthentication("Andy", "auth1".toCharArray()); + + // authenticate with this user details + authenticationService.authenticate("Andy", "auth1".toCharArray()); + + // assert the user is authenticated + assertEquals(dao.getUserNamesAreCaseSensitive() ? "Andy" : "andy", authenticationService.getCurrentUserName()); + // delete the user authentication object + + authenticationService.clearCurrentSecurityContext(); + authenticationService.deleteAuthentication("Andy"); + + // create a new authentication user object + authenticationService.createAuthentication("Andy", "auth2".toCharArray()); + // change the password + authenticationService.setAuthentication("Andy", "auth3".toCharArray()); + // authenticate again to assert password changed + authenticationService.authenticate("Andy", "auth3".toCharArray()); + + try + { + authenticationService.authenticate("Andy", "auth1".toCharArray()); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + try + { + authenticationService.authenticate("Andy", "auth2".toCharArray()); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + // get the ticket that represents the current user authentication + // instance + String ticket = authenticationService.getCurrentTicket(); + // validate our ticket is still valid + authenticationService.validate(ticket); + + // destroy the ticket instance + authenticationService.invalidateTicket(ticket); + try + { + authenticationService.validate(ticket); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + // clear any context and check we are no longer authenticated + authenticationService.clearCurrentSecurityContext(); + assertNull(authenticationService.getCurrentUserName()); + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + + public void testPubAuthenticationService() + { + pubAuthenticationService.createAuthentication("GUEST", "".toCharArray()); + pubAuthenticationService.authenticate("GUEST", "".toCharArray()); + + // create an authentication object e.g. the user + pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray()); + + // authenticate with this user details + pubAuthenticationService.authenticate("Andy", "auth1".toCharArray()); + + // assert the user is authenticated + assertEquals(dao.getUserNamesAreCaseSensitive() ? "Andy" : "andy", authenticationService.getCurrentUserName()); + // delete the user authentication object + + pubAuthenticationService.clearCurrentSecurityContext(); + pubAuthenticationService.deleteAuthentication("Andy"); + + // create a new authentication user object + pubAuthenticationService.createAuthentication("Andy", "auth2".toCharArray()); + // change the password + pubAuthenticationService.setAuthentication("Andy", "auth3".toCharArray()); + // authenticate again to assert password changed + pubAuthenticationService.authenticate("Andy", "auth3".toCharArray()); + + try + { + pubAuthenticationService.authenticate("Andy", "auth1".toCharArray()); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + try + { + pubAuthenticationService.authenticate("Andy", "auth2".toCharArray()); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + // get the ticket that represents the current user authentication + // instance + String ticket = pubAuthenticationService.getCurrentTicket(); + // validate our ticket is still valid + pubAuthenticationService.validate(ticket); + + // destroy the ticket instance + pubAuthenticationService.invalidateTicket(ticket); + try + { + pubAuthenticationService.validate(ticket); + assertNotNull(null); + } + catch (AuthenticationException e) + { + + } + + // clear any context and check we are no longer authenticated + pubAuthenticationService.clearCurrentSecurityContext(); + assertNull(pubAuthenticationService.getCurrentUserName()); + + dao.deleteUser("Andy"); + // assertNull(dao.getUserOrNull("Andy")); + } + + + public void testPassThroughLogin() + { + authenticationService.createAuthentication("Andy", "auth1".toCharArray()); + + authenticationComponent.setCurrentUser("Andy"); + assertEquals(dao.getUserNamesAreCaseSensitive() ? "Andy" : "andy", authenticationService.getCurrentUserName()); + + //authenticationService.deleteAuthentication("andy"); + } + + private String getUserName(Authentication authentication) + { + String username = authentication.getPrincipal().toString(); + + if (authentication.getPrincipal() instanceof UserDetails) + { + username = ((UserDetails) authentication.getPrincipal()).getUsername(); + } + return username; + } + + private NamespacePrefixResolver getNamespacePrefixReolsver(String defaultURI) + { + DynamicNamespacePrefixResolver nspr = new DynamicNamespacePrefixResolver(null); + nspr.registerNamespace(NamespaceService.SYSTEM_MODEL_PREFIX, NamespaceService.SYSTEM_MODEL_1_0_URI); + nspr.registerNamespace(NamespaceService.CONTENT_MODEL_PREFIX, NamespaceService.CONTENT_MODEL_1_0_URI); + nspr.registerNamespace(ContentModel.USER_MODEL_PREFIX, ContentModel.USER_MODEL_URI); + nspr.registerNamespace("namespace", "namespace"); + nspr.registerNamespace(NamespaceService.DEFAULT_PREFIX, defaultURI); + return nspr; + } +} diff --git a/source/java/org/alfresco/repo/security/authentication/DefaultMutableAuthenticationDao.java b/source/java/org/alfresco/repo/security/authentication/DefaultMutableAuthenticationDao.java new file mode 100644 index 0000000000..267d907b4f --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/DefaultMutableAuthenticationDao.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import java.util.Date; + +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.dao.DataAccessException; + +/** + * An authority DAO that has no implementation and should not be called. + * + * @author Andy Hind + */ +public class DefaultMutableAuthenticationDao implements MutableAuthenticationDao +{ + + + /** + * Create a user with the given userName and password + * + * @param userName + * @param rawPassword + * @throws AuthenticationException + */ + public void createUser(String userName, char[] rawPassword) throws AuthenticationException + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Update a user's password. + * + * @param userName + * @param rawPassword + * @throws AuthenticationException + */ + public void updateUser(String userName, char[] rawPassword) throws AuthenticationException + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Delete a user. + * + * @param userName + * @throws AuthenticationException + */ + public void deleteUser(String userName) throws AuthenticationException + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Check is a user exists. + * + * @param userName + * @return + */ + public boolean userExists(String userName) + { + return true; + } + + /** + * Get the store ref where user objects are persisted. + * + * @return + */ + public StoreRef getUserStoreRef() + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Enable/disable a user. + * + * @param userName + * @param enabled + */ + public void setEnabled(String userName, boolean enabled) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Getter for user enabled + * + * @param userName + * @return + */ + public boolean getEnabled(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + + } + + /** + * Set if the account should expire + * + * @param userName + * @param expires + */ + public void setAccountExpires(String userName, boolean expires) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Does the account expire? + * + * @param userName + * @return + */ + + public boolean getAccountExpires(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Has the account expired? + * + * @param userName + * @return + */ + public boolean getAccountHasExpired(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Set if the password expires. + * + * @param userName + * @param expires + */ + public void setCredentialsExpire(String userName, boolean expires) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Do the credentials for the user expire? + * + * @param userName + * @return + */ + public boolean getCredentialsExpire(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Have the credentials for the user expired? + * + * @param userName + * @return + */ + public boolean getCredentialsHaveExpired(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Set if the account is locked. + * + * @param userName + * @param locked + */ + public void setLocked(String userName, boolean locked) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Is the account locked? + * + * @param userName + * @return + */ + public boolean getAccountlocked(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Set the date on which the account expires + * + * @param userName + * @param exipryDate + */ + public void setAccountExpiryDate(String userName, Date exipryDate) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Get the date when this account expires. + * + * @param userName + * @return + */ + public Date getAccountExpiryDate(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Set the date when credentials expire. + * + * @param userName + * @param exipryDate + */ + public void setCredentialsExpiryDate(String userName, Date exipryDate) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Get the date when the credentials/password expire. + * + * @param userName + * @return + */ + public Date getCredentialsExpiryDate(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Get the MD4 password hash + * + * @param userName + * @return + */ + public String getMD4HashedPassword(String userName) + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Are user names case sensitive? + * + * @return + */ + public boolean getUserNamesAreCaseSensitive() + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Return the user details for the specified user + * + * @param user String + * @return UserDetails + * @exception UsernameNotFoundException + * @exception DataAccessException + */ + public UserDetails loadUserByUsername(String arg0) throws UsernameNotFoundException, DataAccessException + { + throw new AlfrescoRuntimeException("Not implemented"); + } + + /** + * Return salt for user + * + * @param user UserDetails + * @return Object + */ + public Object getSalt(UserDetails user) + { + throw new AlfrescoRuntimeException("Not implemented"); + } +} diff --git a/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java b/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java new file mode 100644 index 0000000000..db55b630e8 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.service.cmr.repository.datatype.Duration; +import org.alfresco.util.GUID; +public class InMemoryTicketComponentImpl implements TicketComponent +{ + public static final String GRANTED_AUTHORITY_TICKET_PREFIX = "TICKET_"; + + private boolean ticketsExpire; + + private Duration validDuration; + + private boolean oneOff; + + private HashMap tickets = new HashMap(); + + public InMemoryTicketComponentImpl() + { + super(); + } + + public String getTicket(String userName) throws AuthenticationException + { + Date expiryDate = null; + if (ticketsExpire) + { + expiryDate = Duration.add(new Date(), validDuration); + } + Ticket ticket = new Ticket(ticketsExpire, expiryDate, userName); + tickets.put(ticket.getTicketId(), ticket); + + return GRANTED_AUTHORITY_TICKET_PREFIX + ticket.getTicketId(); + } + + public String validateTicket(String ticketString) throws AuthenticationException + { + if (ticketString.length() < GRANTED_AUTHORITY_TICKET_PREFIX.length()) + { + throw new AuthenticationException(ticketString + " is an invalid ticket format"); + } + + String key = ticketString.substring(GRANTED_AUTHORITY_TICKET_PREFIX.length()); + Ticket ticket = tickets.get(key); + if (ticket == null) + { + throw new AuthenticationException("Missing ticket for " + ticketString); + } + if (ticket.hasExpired()) + { + throw new TicketExpiredException("Ticket expired for " + ticketString); + } + // TODO: Recheck the user details here + // TODO: Strengthen ticket as GUID is predicatble + if(oneOff) + { + tickets.remove(key); + } + return ticket.getUserName(); + } + + public void invalidateTicketById(String ticketString) + { + String key = ticketString.substring(GRANTED_AUTHORITY_TICKET_PREFIX.length()); + tickets.remove(key); + } + + public void invalidateTicketByUser(String userName) + { + Set toRemove = new HashSet(); + + for(String key: tickets.keySet()) + { + Ticket ticket = tickets.get(key); + if(ticket.getUserName().equals(userName)) + { + toRemove.add(ticket.getTicketId()); + } + } + + for(String id: toRemove) + { + tickets.remove(id); + } + } + + + + private static class Ticket + { + private boolean expires; + + private Date expiryDate; + + private String userName; + + private String ticketId; + + Ticket(boolean expires, Date expiryDate, String userName) + { + this.expires = expires; + this.expiryDate = expiryDate; + this.userName = userName; + this.ticketId = GUID.generate(); + } + + /** + * Has the tick expired + * + * @return + */ + boolean hasExpired() + { + if (expires && (expiryDate != null) && (expiryDate.compareTo(new Date()) < 0)) + { + return true; + } + else + { + return false; + } + } + + public boolean equals(Object o) + { + if (o == this) + { + return true; + } + if (!(o instanceof Ticket)) + { + return false; + } + Ticket t = (Ticket) o; + return (this.expires == t.expires) && this.expiryDate.equals(t.expiryDate) && this.userName.equals(t.userName) && this.ticketId.equals(t.ticketId); + } + + public int hashCode() + { + return ticketId.hashCode(); + } + + protected boolean getExpires() + { + return expires; + } + + protected Date getExpiryDate() + { + return expiryDate; + } + + protected String getTicketId() + { + return ticketId; + } + + protected String getUserName() + { + return userName; + } + + } + + + + public void setOneOff(boolean oneOff) + { + this.oneOff = oneOff; + } + + + public void setTicketsExpire(boolean ticketsExpire) + { + this.ticketsExpire = ticketsExpire; + } + + + public void setValidDuration(String validDuration) + { + this.validDuration = new Duration(validDuration); + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoder.java b/source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoder.java new file mode 100644 index 0000000000..10c57f4828 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoder.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import net.sf.acegisecurity.providers.encoding.PasswordEncoder; + +public interface MD4PasswordEncoder extends PasswordEncoder +{ + /** + * Get the MD4 byte array + * + * @param encodedHash + * @return + */ + public byte[] decodeHash(String encodedHash); +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoderImpl.java b/source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoderImpl.java new file mode 100644 index 0000000000..434c722576 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/MD4PasswordEncoderImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; + +import net.sf.acegisecurity.providers.encoding.BaseDigestPasswordEncoder; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +import cryptix.jce.provider.CryptixCrypto; + +/** + *

    + * MD4 implementation of PasswordEncoder. + *

    + * + *

    + * If a null password is presented, it will be treated as an + * empty String ("") password. + *

    + * + *

    + * As MD4 is a one-way hash, the salt can contain any characters. + *

    + */ +public class MD4PasswordEncoderImpl extends BaseDigestPasswordEncoder implements MD4PasswordEncoder +{ + + static + { + try + { + MessageDigest.getInstance("MD4"); + } + catch (NoSuchAlgorithmException e) + { + Security.addProvider(new CryptixCrypto()); + } + } + + + public MD4PasswordEncoderImpl() + { + super(); + // TODO Auto-generated constructor stub + } + + // ~ Methods + // ================================================================ + + public boolean isPasswordValid(String encPass, String rawPass, Object salt) + { + String pass1 = "" + encPass; + String pass2 = encodeInternal(mergePasswordAndSalt(rawPass, salt, false)); + + return pass1.equals(pass2); + } + + public String encodePassword(String rawPass, Object salt) + { + return encodeInternal(mergePasswordAndSalt(rawPass, salt, false)); + } + + private String encodeInternal(String input) + { + if (!getEncodeHashAsBase64()) + { + return new String(Hex.encodeHex(md4(input))); + } + + byte[] encoded = Base64.encodeBase64(md4(input)); + + try + { + return new String(encoded, "UTF8"); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException("UTF8 not supported!", e); + } + } + + private byte[] md4(String input) + { + try + { + MessageDigest digester = MessageDigest.getInstance("MD4"); + return digester.digest(input.getBytes("UnicodeLittleUnmarked")); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e.getMessage(), e); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e.getMessage(), e); + } + } + + public byte[] decodeHash(String encodedHash) + { + if (!getEncodeHashAsBase64()) + { + try + { + return Hex.decodeHex(encodedHash.toCharArray()); + } + catch (DecoderException e) + { + throw new RuntimeException("Unable to decode password hash"); + } + } + else + { + return Base64.decodeBase64(encodedHash.getBytes()); + } + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/MutableAuthenticationDao.java b/source/java/org/alfresco/repo/security/authentication/MutableAuthenticationDao.java new file mode 100644 index 0000000000..b8fdb3f046 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/MutableAuthenticationDao.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import java.util.Date; + +import net.sf.acegisecurity.providers.dao.AuthenticationDao; +import net.sf.acegisecurity.providers.dao.SaltSource; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * A service provider interface to provide both acegi integration via AuthenticationDao and SaltSource + * and mutability support for user definitions. + * + * @author Andy Hind + */ +public interface MutableAuthenticationDao extends AuthenticationDao, SaltSource +{ + /** + * Create a user with the given userName and password + * + * @param userName + * @param rawPassword + * @throws AuthenticationException + */ + public void createUser(String userName, char[] rawPassword) throws AuthenticationException; + + /** + * Update a user's password. + * + * @param userName + * @param rawPassword + * @throws AuthenticationException + */ + public void updateUser(String userName, char[] rawPassword) throws AuthenticationException; + + /** + * Delete a user. + * + * @param userName + * @throws AuthenticationException + */ + public void deleteUser(String userName) throws AuthenticationException; + + /** + * CHeck is a user exists. + * + * @param userName + * @return + */ + public boolean userExists(String userName); + + + /** + * Get the store ref where user objects are persisted. + * + * @return + */ + public StoreRef getUserStoreRef(); + + /** + * Enable/disable a user. + * + * @param userName + * @param enabled + */ + public void setEnabled(String userName, boolean enabled); + + /** + * Getter for user enabled + * + * @param userName + * @return + */ + public boolean getEnabled(String userName); + + /** + * Set if the account should expire + * + * @param userName + * @param expires + */ + public void setAccountExpires(String userName, boolean expires); + + /** + * Does the account expire? + * + * @param userName + * @return + */ + + public boolean getAccountExpires(String userName); + + /** + * Has the account expired? + * + * @param userName + * @return + */ + public boolean getAccountHasExpired(String userName); + + /** + * Set if the password expires. + * + * @param userName + * @param expires + */ + public void setCredentialsExpire(String userName, boolean expires); + + /** + * Do the credentials for the user expire? + * + * @param userName + * @return + */ + public boolean getCredentialsExpire(String userName); + + /** + * Have the credentials for the user expired? + * + * @param userName + * @return + */ + public boolean getCredentialsHaveExpired(String userName); + + /** + * Set if the account is locked. + * + * @param userName + * @param locked + */ + public void setLocked(String userName, boolean locked); + + /** + * Is the account locked? + * + * @param userName + * @return + */ + public boolean getAccountlocked(String userName); + + /** + * Set the date on which the account expires + * + * @param userName + * @param exipryDate + */ + public void setAccountExpiryDate(String userName, Date exipryDate); + + /** + * Get the date when this account expires. + * + * @param userName + * @return + */ + public Date getAccountExpiryDate(String userName); + + /** + * Set the date when credentials expire. + * + * @param userName + * @param exipryDate + */ + public void setCredentialsExpiryDate(String userName, Date exipryDate); + + /** + * Get the date when the credentials/password expire. + * + * @param userName + * @return + */ + public Date getCredentialsExpiryDate(String userName); + + /** + * Get the MD4 password hash + * + * @param userName + * @return + */ + public String getMD4HashedPassword(String userName); + + /** + * Are user names case sensitive? + * + * @return + */ + public boolean getUserNamesAreCaseSensitive(); + +} diff --git a/source/java/org/alfresco/repo/security/authentication/NTLMMode.java b/source/java/org/alfresco/repo/security/authentication/NTLMMode.java new file mode 100644 index 0000000000..4b271bc1a9 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/NTLMMode.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +public enum NTLMMode +{ + PASS_THROUGH, MD4_PROVIDER, NONE +} diff --git a/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java new file mode 100644 index 0000000000..87a01b173d --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.dao.User; +import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; +import net.sf.acegisecurity.providers.encoding.PasswordEncoder; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.QueryParameterDefImpl; +import org.alfresco.repo.security.permissions.PermissionServiceSPI; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.springframework.dao.DataAccessException; + +public class RepositoryAuthenticationDao implements MutableAuthenticationDao +{ + + private static final String SYSTEM_FOLDER = "/sys:system"; + + private static final String PEOPLE_FOLDER = SYSTEM_FOLDER + "/sys:people"; + + private NodeService nodeService; + + private NamespacePrefixResolver namespacePrefixResolver; + + private DictionaryService dictionaryService; + + private SearchService searchService; + + private PasswordEncoder passwordEncoder; + + private StoreRef userStoreRef; + + private boolean userNamesAreCaseSensitive; + + public boolean getUserNamesAreCaseSensitive() + { + return userNamesAreCaseSensitive; + } + + public void setUserNamesAreCaseSensitive(boolean userNamesAreCaseSensitive) + { + this.userNamesAreCaseSensitive = userNamesAreCaseSensitive; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + + + public void setNamespaceService(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPasswordEncoder(PasswordEncoder passwordEncoder) + { + this.passwordEncoder = passwordEncoder; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public UserDetails loadUserByUsername(String caseSensitiveUserName) throws UsernameNotFoundException, DataAccessException + { + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName: caseSensitiveUserName.toLowerCase(); + NodeRef userRef = getUserOrNull(userNamesAreCaseSensitive ? userName: userName.toLowerCase()); + if (userRef == null) + { + throw new UsernameNotFoundException("Could not find user by userName: " + caseSensitiveUserName); + } + + Map properties = nodeService.getProperties(userRef); + String password = DefaultTypeConverter.INSTANCE.convert(String.class, properties + .get(ContentModel.PROP_PASSWORD)); + + GrantedAuthority[] gas = new GrantedAuthority[1]; + gas[0] = new GrantedAuthorityImpl("ROLE_AUTHENTICATED"); + + UserDetails ud = new User(userName, password, getEnabled(userName), !getAccountHasExpired(userName), + !getCredentialsHaveExpired(userName), !getAccountlocked(userName), gas); + return ud; + } + + public NodeRef getUserOrNull(String caseSensitiveUserName) + { + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName: caseSensitiveUserName.toLowerCase(); + NodeRef rootNode = nodeService.getRootNode(getUserStoreRef()); + QueryParameterDefinition[] defs = new QueryParameterDefinition[1]; + DataTypeDefinition text = dictionaryService.getDataType(DataTypeDefinition.TEXT); + defs[0] = new QueryParameterDefImpl(QName.createQName("usr", "var", namespacePrefixResolver), text, true, + userName); + List results = searchService.selectNodes(rootNode, PEOPLE_FOLDER + + "/usr:user[@usr:username = $usr:var ]", defs, namespacePrefixResolver, false); + if (results.size() != 1) + { + return null; + } + return results.get(0); + } + + public void createUser(String caseSensitiveUserName, char[] rawPassword) throws AuthenticationException + { + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName: caseSensitiveUserName.toLowerCase(); + NodeRef userRef = getUserOrNull(userName); + if (userRef != null) + { + throw new AuthenticationException("User already exists: " + userName); + } + NodeRef typesNode = getOrCreateTypeLocation(); + Map properties = new HashMap(); + properties.put(ContentModel.PROP_USER_USERNAME, userName); + String salt = null; // GUID.generate(); + properties.put(ContentModel.PROP_SALT, salt); + properties.put(ContentModel.PROP_PASSWORD, passwordEncoder.encodePassword(new String(rawPassword), salt)); + properties.put(ContentModel.PROP_ACCOUNT_EXPIRES, Boolean.valueOf(false)); + properties.put(ContentModel.PROP_CREDENTIALS_EXPIRE, Boolean.valueOf(false)); + properties.put(ContentModel.PROP_ENABLED, Boolean.valueOf(true)); + properties.put(ContentModel.PROP_ACCOUNT_LOCKED, Boolean.valueOf(false)); + nodeService.createNode(typesNode, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_USER, ContentModel.TYPE_USER, + properties); + + } + + private NodeRef getOrCreateTypeLocation() + { + NodeRef rootNode = nodeService.getRootNode(getUserStoreRef()); + List results = nodeService.getChildAssocs( + rootNode, + RegexQNamePattern.MATCH_ALL, + QName.createQName("sys", "system", namespacePrefixResolver)); + NodeRef sysNode = null; + if (results.size() == 0) + { + sysNode = nodeService.createNode(rootNode, ContentModel.ASSOC_CHILDREN, + QName.createQName("sys", "system", namespacePrefixResolver), ContentModel.TYPE_CONTAINER) + .getChildRef(); + } + else + { + sysNode = results.get(0).getChildRef(); + } + results = nodeService.getChildAssocs( + sysNode, + RegexQNamePattern.MATCH_ALL, + QName.createQName("sys", "people", namespacePrefixResolver)); + NodeRef typesNode = null; + if (results.size() == 0) + { + typesNode = nodeService.createNode(sysNode, ContentModel.ASSOC_CHILDREN, + QName.createQName("sys", "people", namespacePrefixResolver), ContentModel.TYPE_CONTAINER) + .getChildRef(); + } + else + { + typesNode = results.get(0).getChildRef(); + } + return typesNode; + } + + public void updateUser(String userName, char[] rawPassword) throws AuthenticationException + { + NodeRef userRef = getUserOrNull(userName); + if (userRef == null) + { + throw new AuthenticationException("User does not exist: " + userName); + } + Map properties = nodeService.getProperties(userRef); + String salt = null; // GUID.generate(); + properties.remove(ContentModel.PROP_SALT); + properties.put(ContentModel.PROP_SALT, salt); + properties.remove(ContentModel.PROP_PASSWORD); + properties.put(ContentModel.PROP_PASSWORD, passwordEncoder.encodePassword(new String(rawPassword), salt)); + nodeService.setProperties(userRef, properties); + } + + public void deleteUser(String userName) throws AuthenticationException + { + NodeRef userRef = getUserOrNull(userName); + if (userRef == null) + { + throw new AuthenticationException("User does not exist: " + userName); + } + nodeService.deleteNode(userRef); + } + + public synchronized StoreRef getUserStoreRef() + { + if (userStoreRef == null) + { + userStoreRef = new StoreRef("user", "alfrescoUserStore"); + } + if (!nodeService.exists(userStoreRef)) + { + nodeService.createStore(userStoreRef.getProtocol(), userStoreRef.getIdentifier()); + } + + return userStoreRef; + } + + public Object getSalt(UserDetails userDetails) + { + // NodeRef userRef = getUserOrNull(userDetails.getUsername()); + // if (userRef == null) + // { + // throw new UsernameNotFoundException("Could not find user by userName: + // " + userDetails.getUsername()); + // } + // + // Map properties = + // nodeService.getProperties(userRef); + // + // String salt = DefaultTypeConverter.INSTANCE.convert(String.class, + // properties.get(QName.createQName("usr", "salt", + // namespacePrefixResolver))); + // + // return salt; + return null; + } + + public boolean userExists(String userName) + { + return (getUserOrNull(userName) != null); + } + + public boolean getAccountExpires(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return false; + } + Serializable ser = nodeService.getProperty(userNode, ContentModel.PROP_ACCOUNT_EXPIRES); + if (ser == null) + { + return false; + } + else + { + return DefaultTypeConverter.INSTANCE.booleanValue(ser); + } + } + + public Date getAccountExpiryDate(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return null; + } + if (DefaultTypeConverter.INSTANCE.booleanValue(nodeService.getProperty(userNode, + ContentModel.PROP_ACCOUNT_EXPIRES))) + { + return DefaultTypeConverter.INSTANCE.convert(Date.class, nodeService.getProperty(userNode, + ContentModel.PROP_ACCOUNT_EXPIRY_DATE)); + } + else + { + return null; + } + } + + public boolean getAccountHasExpired(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return false; + } + if (DefaultTypeConverter.INSTANCE.booleanValue(nodeService.getProperty(userNode, + ContentModel.PROP_ACCOUNT_EXPIRES))) + { + Date date = DefaultTypeConverter.INSTANCE.convert(Date.class, nodeService.getProperty(userNode, + ContentModel.PROP_ACCOUNT_EXPIRY_DATE)); + if (date == null) + { + return false; + } + else + { + return (date.compareTo(new Date()) < 1); + } + } + else + { + return false; + } + } + + public boolean getAccountlocked(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return false; + } + Serializable ser = nodeService.getProperty(userNode, ContentModel.PROP_ACCOUNT_LOCKED); + if (ser == null) + { + return false; + } + else + { + return DefaultTypeConverter.INSTANCE.booleanValue(ser); + } + } + + public boolean getCredentialsExpire(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return false; + } + Serializable ser = nodeService.getProperty(userNode, ContentModel.PROP_CREDENTIALS_EXPIRE); + if (ser == null) + { + return false; + } + else + { + return DefaultTypeConverter.INSTANCE.booleanValue(ser); + } + } + + public Date getCredentialsExpiryDate(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return null; + } + if (DefaultTypeConverter.INSTANCE.booleanValue(nodeService.getProperty(userNode, + ContentModel.PROP_CREDENTIALS_EXPIRE))) + { + return DefaultTypeConverter.INSTANCE.convert(Date.class, nodeService.getProperty(userNode, + ContentModel.PROP_CREDENTIALS_EXPIRY_DATE)); + } + else + { + return null; + } + } + + public boolean getCredentialsHaveExpired(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return false; + } + if (DefaultTypeConverter.INSTANCE.booleanValue(nodeService.getProperty(userNode, + ContentModel.PROP_CREDENTIALS_EXPIRE))) + { + Date date = DefaultTypeConverter.INSTANCE.convert(Date.class, nodeService.getProperty(userNode, + ContentModel.PROP_CREDENTIALS_EXPIRY_DATE)); + if (date == null) + { + return false; + } + else + { + return (date.compareTo(new Date()) < 1); + } + } + else + { + return false; + } + } + + public boolean getEnabled(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return false; + } + Serializable ser = nodeService.getProperty(userNode, ContentModel.PROP_ENABLED); + if (ser == null) + { + return true; + } + else + { + return DefaultTypeConverter.INSTANCE.booleanValue(ser); + } + } + + public void setAccountExpires(String userName, boolean expires) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + throw new AuthenticationException("User not found: " + userName); + } + nodeService.setProperty(userNode, ContentModel.PROP_ACCOUNT_EXPIRES, Boolean.valueOf(expires)); + } + + public void setAccountExpiryDate(String userName, Date exipryDate) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + throw new AuthenticationException("User not found: " + userName); + } + nodeService.setProperty(userNode, ContentModel.PROP_ACCOUNT_EXPIRY_DATE, exipryDate); + + } + + public void setCredentialsExpire(String userName, boolean expires) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + throw new AuthenticationException("User not found: " + userName); + } + nodeService.setProperty(userNode, ContentModel.PROP_CREDENTIALS_EXPIRE, Boolean.valueOf(expires)); + } + + public void setCredentialsExpiryDate(String userName, Date exipryDate) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + throw new AuthenticationException("User not found: " + userName); + } + nodeService.setProperty(userNode, ContentModel.PROP_CREDENTIALS_EXPIRY_DATE, exipryDate); + + } + + public void setEnabled(String userName, boolean enabled) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + throw new AuthenticationException("User not found: " + userName); + } + nodeService.setProperty(userNode, ContentModel.PROP_ENABLED, Boolean.valueOf(enabled)); + } + + public void setLocked(String userName, boolean locked) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + throw new AuthenticationException("User not found: " + userName); + } + nodeService.setProperty(userNode, ContentModel.PROP_ACCOUNT_LOCKED, Boolean.valueOf(locked)); + } + + public String getMD4HashedPassword(String userName) + { + NodeRef userNode = getUserOrNull(userName); + if (userNode == null) + { + return null; + } + else + { + String password = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(userNode, + ContentModel.PROP_PASSWORD)); + return password; + } + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/TicketComponent.java b/source/java/org/alfresco/repo/security/authentication/TicketComponent.java new file mode 100644 index 0000000000..25f314e01a --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/TicketComponent.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + + +/** + * Manage authentication tickets + * + * @author andyh + * + */ +public interface TicketComponent +{ + /** + * Register a ticket + * + * @param authentication + * @return + * @throws AuthenticationException + */ + public String getTicket(String userName) throws AuthenticationException; + + /** + * Check that a certificate is valid and can be used in place of a login. + * + * Tickets may be rejected because: + *
      + *
    1. The certificate does not exists + *
    2. The status of the user has changed + *
        + *
      1. The user is locked + *
      2. The account has expired + *
      3. The credentials have expired + *
      4. The account is disabled + *
      + *
    3. The ticket may have expired + *
        + *
      1. The ticked my be invalid by timed expiry + *
      2. An attemp to reuse a once only ticket + *
      + *
    + * + * @param authentication + * @return + * @throws AuthenticationException + */ + public String validateTicket(String ticket) throws AuthenticationException; + + public void invalidateTicketById(String ticket); + + public void invalidateTicketByUser(String userName); +} diff --git a/source/java/org/alfresco/repo/security/authentication/TicketExpiredException.java b/source/java/org/alfresco/repo/security/authentication/TicketExpiredException.java new file mode 100644 index 0000000000..0c441c5cb2 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/TicketExpiredException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authentication; + +public class TicketExpiredException extends AuthenticationException +{ + + /** + * + */ + private static final long serialVersionUID = 3257572801815590969L; + + public TicketExpiredException(String msg) + { + super(msg); + } + + public TicketExpiredException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/userModel.xml b/source/java/org/alfresco/repo/security/authentication/userModel.xml new file mode 100644 index 0000000000..f4a13c7fe3 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/userModel.xml @@ -0,0 +1,90 @@ + + + Alfresco User Model + Alfresco + 2005-08-16 + 0.1 + + + + + + + + + + + + + + + Alfreco Authority Abstract Type + sys:base + + + + Alfresco User Type + usr:authority + + + d:text + + + d:text + + + d:boolean + + + d:boolean + + + d:datetime + + + d:boolean + + + d:datetime + + + d:boolean + + + d:text + + + + + + Alfresco Authority Type + usr:authority + + + d:text + + + d:text + true + + + + + + false + true + + + usr:authority + false + true + + false + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceImpl.java b/source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceImpl.java new file mode 100644 index 0000000000..e38be6d639 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceImpl.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authority; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; + +/** + * The default implementation of the authority service. + * + * @author Andy Hind + */ +public class SimpleAuthorityServiceImpl implements AuthorityService +{ + private PersonService personService; + + private NodeService nodeService; + + private Set adminSet = Collections.singleton(PermissionService.ADMINISTRATOR_AUTHORITY); + + private Set guestSet = Collections.singleton(PermissionService.GUEST); + + private Set allSet = Collections.singleton(PermissionService.ALL_AUTHORITIES); + + private Set adminUsers; + + private AuthenticationComponent authenticationComponent; + + public SimpleAuthorityServiceImpl() + { + super(); + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * Currently the admin authority is granted only to the ALFRESCO_ADMIN_USER + * user. + */ + public boolean hasAdminAuthority() + { + String currentUserName = authenticationComponent.getCurrentUserName(); + return ((currentUserName != null) && adminUsers.contains(currentUserName)); + } + + // IOC + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + public void setAdminUsers(Set adminUsers) + { + this.adminUsers = adminUsers; + } + + public Set getAuthorities() + { + Set authorities = new HashSet(); + String currentUserName = authenticationComponent.getCurrentUserName(); + if (adminUsers.contains(currentUserName)) + { + authorities.addAll(adminSet); + } + authorities.addAll(allSet); + return authorities; + } + + public Set getAllAuthorities(AuthorityType type) + { + Set authorities = new HashSet(); + switch (type) + { + case ADMIN: + authorities.addAll(adminSet); + break; + case EVERYONE: + authorities.addAll(allSet); + break; + case GUEST: + authorities.addAll(guestSet); + break; + case GROUP: + authorities.addAll(allSet); + break; + case OWNER: + break; + case ROLE: + break; + case USER: + for (NodeRef personRef : personService.getAllPeople()) + { + authorities.add(DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(personRef, + ContentModel.PROP_USERNAME))); + } + break; + default: + break; + } + return authorities; + } + + public void addAuthority(String parentName, String childName) + { + + } + + + public String createAuthority(AuthorityType type, String parentName, String shortName) + { + return ""; + } + + public void deleteAuthority(String name) + { + + } + + public Set getAllRootAuthorities(AuthorityType type) + { + return getAllAuthorities(type); + } + + public Set getContainedAuthorities(AuthorityType type, String name, boolean immediate) + { + return Collections.emptySet(); + } + + public Set getContainingAuthorities(AuthorityType type, String name, boolean immediate) + { + return Collections.emptySet(); + } + + public String getName(AuthorityType type, String shortName) + { + if (type.isFixedString()) + { + return type.getFixedString(); + } + else if (type.isPrefixed()) + { + return type.getPrefixString() + shortName; + } + else + { + return shortName; + } + } + + public String getShortName(String name) + { + AuthorityType type = AuthorityType.getAuthorityType(name); + if (type.isFixedString()) + { + return ""; + } + else if (type.isPrefixed()) + { + return name.substring(type.getPrefixString().length()); + } + else + { + return name; + } + + } + + public void removeAuthority(String parentName, String childName) + { + + } + +} diff --git a/source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceTest.java b/source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceTest.java new file mode 100644 index 0000000000..0ef23d3a1a --- /dev/null +++ b/source/java/org/alfresco/repo/security/authority/SimpleAuthorityServiceTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.authority; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.MutableAuthenticationDao; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class SimpleAuthorityServiceTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private AuthenticationComponent authenticationComponent; + + private AuthenticationService authenticationService; + + private AuthorityService authorityService; + + private AuthorityService pubAuthorityService; + + private MutableAuthenticationDao authenticationDAO; + + private PersonService personService; + + private UserTransaction tx; + + public SimpleAuthorityServiceTest() + { + super(); + + } + + public void setUp() throws Exception + { + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + authenticationService = (AuthenticationService) ctx.getBean("authenticationService"); + authorityService = (AuthorityService) ctx.getBean("authorityService"); + pubAuthorityService = (AuthorityService) ctx.getBean("AuthorityService"); + personService = (PersonService) ctx.getBean("personService"); + authenticationDAO = (MutableAuthenticationDao) ctx.getBean("alfDaoImpl"); + + this.authenticationComponent.setSystemUserAsCurrentUser(); + + TransactionService transactionService = (TransactionService) ctx.getBean(ServiceRegistry.TRANSACTION_SERVICE + .getLocalName()); + tx = transactionService.getUserTransaction(); + tx.begin(); + + if (!authenticationDAO.userExists("andy")) + { + authenticationService.createAuthentication("andy", "andy".toCharArray()); + } + + if (!authenticationDAO.userExists("admin")) + { + authenticationService.createAuthentication("admin", "admin".toCharArray()); + } + + if (!authenticationDAO.userExists("administrator")) + { + authenticationService.createAuthentication("administrator", "administrator".toCharArray()); + } + } + + @Override + protected void tearDown() throws Exception + { + authenticationService.clearCurrentSecurityContext(); + tx.rollback(); + super.tearDown(); + } + + public void testNonAdminUser() + { + authenticationComponent.setCurrentUser("andy"); + assertFalse(authorityService.hasAdminAuthority()); + assertFalse(pubAuthorityService.hasAdminAuthority()); + assertEquals(1, authorityService.getAuthorities().size()); + } + + public void testAdminUser() + { + authenticationComponent.setCurrentUser("admin"); + assertTrue(authorityService.hasAdminAuthority()); + assertTrue(pubAuthorityService.hasAdminAuthority()); + assertEquals(2, authorityService.getAuthorities().size()); + + authenticationComponent.setCurrentUser("administrator"); + assertTrue(authorityService.hasAdminAuthority()); + assertTrue(pubAuthorityService.hasAdminAuthority()); + assertEquals(2, authorityService.getAuthorities().size()); + } + + public void testAuthorities() + { + assertEquals(1, pubAuthorityService.getAllAuthorities(AuthorityType.ADMIN).size()); + assertTrue(pubAuthorityService.getAllAuthorities(AuthorityType.ADMIN).contains( + PermissionService.ADMINISTRATOR_AUTHORITY)); + assertEquals(1, pubAuthorityService.getAllAuthorities(AuthorityType.EVERYONE).size()); + assertTrue(pubAuthorityService.getAllAuthorities(AuthorityType.EVERYONE).contains( + PermissionService.ALL_AUTHORITIES)); + assertEquals(1, pubAuthorityService.getAllAuthorities(AuthorityType.GROUP).size()); + assertTrue(pubAuthorityService.getAllAuthorities(AuthorityType.GROUP).contains( + PermissionService.ALL_AUTHORITIES)); + assertEquals(1, pubAuthorityService.getAllAuthorities(AuthorityType.GUEST).size()); + assertTrue(pubAuthorityService.getAllAuthorities(AuthorityType.GUEST).contains(PermissionService.GUEST)); + assertEquals(0, pubAuthorityService.getAllAuthorities(AuthorityType.OWNER).size()); + assertEquals(0, pubAuthorityService.getAllAuthorities(AuthorityType.ROLE).size()); + assertEquals(personService.getAllPeople().size(), pubAuthorityService.getAllAuthorities(AuthorityType.USER) + .size()); + + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/AccessDeniedException.java b/source/java/org/alfresco/repo/security/permissions/AccessDeniedException.java new file mode 100644 index 0000000000..2889b6a0ed --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/AccessDeniedException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Runtime access denied exception that is exposed + * + * @author Andy Hind + */ +public class AccessDeniedException extends AlfrescoRuntimeException +{ + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = -4451661115250681152L; + + public AccessDeniedException(String msg) + { + super(msg); + } + + public AccessDeniedException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/AuthorityReference.java b/source/java/org/alfresco/repo/security/permissions/AuthorityReference.java new file mode 100644 index 0000000000..b601b1c026 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/AuthorityReference.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +/** + * A simple reference to a string authority. + * + * @author Andy Hind + */ +public interface AuthorityReference +{ + public String getAuthority(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/DynamicAuthority.java b/source/java/org/alfresco/repo/security/permissions/DynamicAuthority.java new file mode 100644 index 0000000000..48cd531ef2 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/DynamicAuthority.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * The interface for a dynamic authority provider e.g. for the owner of a node + * or any other authority that is determined by the context rather than just a + * node. + * + * @author Andy Hind + */ +public interface DynamicAuthority +{ + /** + * Is this authority granted to the given user for this node ref? + * + * @param nodeRef + * @param userName + * @return + */ + public boolean hasAuthority(NodeRef nodeRef, String userName); + + /** + * If this authority is granted this method provides the string + * representation of the granted authority. + * + * @return + */ + public String getAuthority(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/NodePermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/NodePermissionEntry.java new file mode 100644 index 0000000000..7bdc3f3717 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/NodePermissionEntry.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Encapsulate how permissions are globally inherited between nodes. + * + * @author andyh + */ +public interface NodePermissionEntry +{ + /** + * Get the node ref. + * + * @return + */ + public NodeRef getNodeRef(); + + /** + * Does the node inherit permissions from its primary parent? + * + * @return + */ + public boolean inheritPermissions(); + + + /** + * Get the permission entries set for this node. + * + * @return + */ + public Set getPermissionEntries(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/PermissionEntry.java new file mode 100644 index 0000000000..e778cdb735 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * A single permission entry defined against a node. + * + * @author andyh + */ +public interface PermissionEntry +{ + /** + * Get the permission definition. + * + * This may be null. Null implies that the settings apply to all permissions + * + * @return + */ + public PermissionReference getPermissionReference(); + + /** + * Get the authority to which this entry applies This could be the string + * value of a username, group, role or any other authority assigned to the + * authorisation. + * + * If null then this applies to all. + * + * @return + */ + public String getAuthority(); + + /** + * Get the node ref for the node to which this permission applies. + * + * This can only be null for a global permission + * + * @return + */ + public NodeRef getNodeRef(); + + /** + * Is permissions denied? + * + */ + public boolean isDenied(); + + /** + * Is permission allowed? + * + */ + public boolean isAllowed(); + + /** + * Get the Access enum value + * + * @return + */ + public AccessStatus getAccessStatus(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionReference.java b/source/java/org/alfresco/repo/security/permissions/PermissionReference.java new file mode 100644 index 0000000000..f984df6dfb --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionReference.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +import org.alfresco.service.namespace.QName; + +/** + * A Permission is a named permission against a type or aspect which is defined + * by QName. So a permission string is scoped by type. + * + * @author Andy Hind + */ +public interface PermissionReference +{ + + /** + * Get the QName of the type or aspect against which the permission is + * defined. + * + * @return + */ + public QName getQName(); + + /** + * Get the name of the permission + * + * @return + */ + public String getName(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionServiceSPI.java b/source/java/org/alfresco/repo/security/permissions/PermissionServiceSPI.java new file mode 100644 index 0000000000..8d43527c95 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionServiceSPI.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions; + +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; + +/** + * The public API for a permission service + * + * The implementation may be changed in the application configuration + * + * @author Andy Hind + */ +public interface PermissionServiceSPI extends PermissionService +{ + /** + * Get the All Permission + * + * @return the All permission + */ + public PermissionReference getAllPermissionReference(); + + /** + * Get the permissions that can be set for a given type + * + * @param nodeRef + * @return + */ + public Set getSettablePermissionReferences(QName type); + + /** + * Get the permissions that can be set for a given type + * + * @param nodeRef + * @return + */ + public Set getSettablePermissionReferences(NodeRef nodeRef); + + /** + * Get the permissions that have been set on the given node (it knows + * nothing of the parent permissions) + * + * @param nodeRef + * @return + */ + public NodePermissionEntry getSetPermissions(NodeRef nodeRef); + + /** + * Check that the given authentication has a particular permission for the + * given node. (The default behaviour is to inherit permissions) + * + * @param nodeRef + * @param perm + * @return + */ + public AccessStatus hasPermission(NodeRef nodeRef, PermissionReference perm); + + /** + * Where is the permission set that controls the behaviour for the given + * permission for the given authentication to access the specified name. + * + * @param nodeRef + * @param auth + * @param perm + * @return + */ + public NodePermissionEntry explainPermission(NodeRef nodeRef, PermissionReference perm); + + /** + * Delete the permissions defined by the nodePermissionEntry + * @param nodePermissionEntry + */ + public void deletePermissions(NodePermissionEntry nodePermissionEntry); + + /** + * Delete a single permission entry + * @param permissionEntry + */ + public void deletePermission(PermissionEntry permissionEntry); + + /** + * Add or set a permission entry on a node. + * + * @param permissionEntry + */ + public void setPermission(PermissionEntry permissionEntry); + + /** + * Set the permissions on a node. + * + * @param nodePermissionEntry + */ + public void setPermission(NodePermissionEntry nodePermissionEntry); + + /** + * Get the permission reference for the given data type and permission name. + * + * @param qname - may be null if the permission name is unique + * @param permissionName + * @return + */ + public PermissionReference getPermissionReference(QName qname, String permissionName); + + /** + * Get the permission reference by permission name. + * + * @param permissionName + * @return + */ + public PermissionReference getPermissionReference(String permissionName); + + + /** + * Get the string that can be used to identify the given permission reference. + * + * @param permissionReference + * @return + */ + public String getPermission(PermissionReference permissionReference); + + public void deletePermissions(String recipient); +} diff --git a/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java b/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java new file mode 100644 index 0000000000..40cd3b8d36 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.dynamic; + +import org.alfresco.repo.security.permissions.DynamicAuthority; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.springframework.beans.factory.InitializingBean; + + +public class LockOwnerDynamicAuthority implements DynamicAuthority, InitializingBean +{ + + private LockService lockService; + + public LockOwnerDynamicAuthority() + { + super(); + } + + public boolean hasAuthority(NodeRef nodeRef, String userName) + { + return lockService.getLockStatus(nodeRef) == LockStatus.LOCK_OWNER; + } + + public String getAuthority() + { + return PermissionService.LOCK_OWNER_AUTHORITY; + } + + public void afterPropertiesSet() throws Exception + { + if(lockService == null) + { + throw new IllegalStateException("A lock service must be set"); + } + + } + + public void setLockService(LockService lockService) + { + this.lockService = lockService; + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthorityTest.java b/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthorityTest.java new file mode 100644 index 0000000000..6504f772a7 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthorityTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.dynamic; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.MutableAuthenticationDao; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class LockOwnerDynamicAuthorityTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private NodeService nodeService; + + private AuthenticationService authenticationService; + + private AuthenticationComponent authenticationComponent; + + private MutableAuthenticationDao authenticationDAO; + + private LockService lockService; + + private NodeRef rootNodeRef; + + private UserTransaction userTransaction; + + private PermissionService permissionService; + + private LockOwnerDynamicAuthority dynamicAuthority; + + public LockOwnerDynamicAuthorityTest() + { + super(); + } + + public LockOwnerDynamicAuthorityTest(String arg0) + { + super(arg0); + } + + public void setUp() throws Exception + { + nodeService = (NodeService) ctx.getBean("nodeService"); + authenticationService = (AuthenticationService) ctx.getBean("authenticationService"); + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + lockService = (LockService) ctx.getBean("lockService"); + permissionService = (PermissionService) ctx.getBean("permissionService"); + authenticationDAO = (MutableAuthenticationDao) ctx.getBean("alfDaoImpl"); + + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + + TransactionService transactionService = (TransactionService) ctx.getBean(ServiceRegistry.TRANSACTION_SERVICE + .getLocalName()); + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + permissionService.setPermission(rootNodeRef, PermissionService.ALL_AUTHORITIES, PermissionService.ADD_CHILDREN, + true); + + if (authenticationDAO.userExists("andy")) + { + authenticationService.deleteAuthentication("andy"); + } + authenticationService.createAuthentication("andy", "andy".toCharArray()); + if (authenticationDAO.userExists("lemur")) + { + authenticationService.deleteAuthentication("lemur"); + } + authenticationService.createAuthentication("lemur", "lemur".toCharArray()); + if (authenticationDAO.userExists("frog")) + { + authenticationService.deleteAuthentication("frog"); + } + authenticationService.createAuthentication("frog", "frog".toCharArray()); + + dynamicAuthority = new LockOwnerDynamicAuthority(); + dynamicAuthority.setLockService(lockService); + + authenticationComponent.clearCurrentSecurityContext(); + } + + @Override + protected void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + userTransaction.rollback(); + super.tearDown(); + } + + public void testSetup() + { + assertNotNull(nodeService); + assertNotNull(authenticationService); + assertNotNull(lockService); + } + + public void testUnSet() + { + permissionService.setPermission(rootNodeRef, "andy", PermissionService.ALL_PERMISSIONS, true); + authenticationService.authenticate("andy", "andy".toCharArray()); + assertEquals(LockStatus.NO_LOCK, lockService.getLockStatus(rootNodeRef)); + authenticationService.clearCurrentSecurityContext(); + } + + public void testPermissionWithNoLockAspect() + { + authenticationService.authenticate("andy", "andy".toCharArray()); + NodeRef testNode = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_PERSON, + ContentModel.TYPE_CMOBJECT, null).getChildRef(); + assertNotNull(testNode); + permissionService.setPermission(rootNodeRef, "andy", PermissionService.ALL_PERMISSIONS, true); + + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rootNodeRef, + PermissionService.LOCK)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(rootNodeRef, + PermissionService.UNLOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rootNodeRef, PermissionService.CHECK_OUT)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(rootNodeRef, PermissionService.CHECK_IN)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(rootNodeRef, PermissionService.CANCEL_CHECK_OUT)); + + } + + public void testPermissionWithLockAspect() + { + permissionService.setPermission(rootNodeRef, "andy", PermissionService.ALL_PERMISSIONS, true); + permissionService.setPermission(rootNodeRef, "lemur", PermissionService.CHECK_OUT, true); + permissionService.setPermission(rootNodeRef, "lemur", PermissionService.WRITE, true); + permissionService.setPermission(rootNodeRef, "lemur", PermissionService.READ, true); + permissionService.setPermission(rootNodeRef, "frog", PermissionService.CHECK_OUT, true); + permissionService.setPermission(rootNodeRef, "frog", PermissionService.WRITE, true); + permissionService.setPermission(rootNodeRef, "frog", PermissionService.READ, true); + authenticationService.authenticate("andy", "andy".toCharArray()); + NodeRef testNode = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_PERSON, + ContentModel.TYPE_CMOBJECT, null).getChildRef(); + lockService.lock(testNode, LockType.READ_ONLY_LOCK); + + + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, + PermissionService.LOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, + PermissionService.UNLOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CHECK_OUT)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CHECK_IN)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CANCEL_CHECK_OUT)); + + authenticationService.authenticate("lemur", "lemur".toCharArray()); + + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, + PermissionService.LOCK)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testNode, + PermissionService.UNLOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CHECK_OUT)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testNode, PermissionService.CHECK_IN)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testNode, PermissionService.CANCEL_CHECK_OUT)); + + authenticationService.authenticate("andy", "andy".toCharArray()); + lockService.unlock(testNode); + authenticationService.authenticate("lemur", "lemur".toCharArray()); + lockService.lock(testNode, LockType.READ_ONLY_LOCK); + + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, + PermissionService.LOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, + PermissionService.UNLOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CHECK_OUT)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CHECK_IN)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CANCEL_CHECK_OUT)); + + + authenticationService.authenticate("frog", "frog".toCharArray()); + + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, + PermissionService.LOCK)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testNode, + PermissionService.UNLOCK)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testNode, PermissionService.CHECK_OUT)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testNode, PermissionService.CHECK_IN)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testNode, PermissionService.CANCEL_CHECK_OUT)); + + } + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/dynamic/OwnerDynamicAuthority.java b/source/java/org/alfresco/repo/security/permissions/dynamic/OwnerDynamicAuthority.java new file mode 100644 index 0000000000..0ec6193e43 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/dynamic/OwnerDynamicAuthority.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.dynamic; + +import org.alfresco.repo.security.permissions.DynamicAuthority; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.EqualsHelper; +import org.springframework.beans.factory.InitializingBean; + +public class OwnerDynamicAuthority implements DynamicAuthority, InitializingBean +{ + private OwnableService ownableService; + + public OwnerDynamicAuthority() + { + super(); + } + + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + public void afterPropertiesSet() throws Exception + { + if (ownableService == null) + { + throw new IllegalArgumentException("There must be an ownable service"); + } + } + + public boolean hasAuthority(NodeRef nodeRef, String userName) + { + return EqualsHelper.nullSafeEquals(ownableService.getOwner(nodeRef), userName); + } + + public String getAuthority() + { + return PermissionService.OWNER_AUTHORITY; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/AbstractNodePermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/AbstractNodePermissionEntry.java new file mode 100644 index 0000000000..2db0f7ddde --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/AbstractNodePermissionEntry.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.repo.security.permissions.NodePermissionEntry; + + +/** + * This class provides common support for hash code and equality. + * + * @author andyh + */ +public abstract class AbstractNodePermissionEntry implements + NodePermissionEntry +{ + + public AbstractNodePermissionEntry() + { + super(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof AbstractNodePermissionEntry)) + { + return false; + } + AbstractNodePermissionEntry other = (AbstractNodePermissionEntry) o; + + return this.getNodeRef().equals(other.getNodeRef()) + && (this.inheritPermissions() == other.inheritPermissions()) + && (this.getPermissionEntries().equals(other.getPermissionEntries())); + } + + @Override + public int hashCode() + { + return getNodeRef().hashCode(); + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionEntry.java new file mode 100644 index 0000000000..5429bd850f --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.util.EqualsHelper; + +/** + * This class provides common support for hash code and equality. + * + * @author andyh + */ +public abstract class AbstractPermissionEntry implements PermissionEntry +{ + + public AbstractPermissionEntry() + { + super(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof AbstractPermissionEntry)) + { + return false; + } + AbstractPermissionEntry other = (AbstractPermissionEntry) o; + return EqualsHelper.nullSafeEquals(this.getNodeRef(), + other.getNodeRef()) + && EqualsHelper.nullSafeEquals(this.getPermissionReference(), + other.getPermissionReference()) + && EqualsHelper.nullSafeEquals(this.getAuthority(), other.getAuthority()) + && EqualsHelper.nullSafeEquals(this.getAccessStatus(), other.getAccessStatus()); + } + + @Override + public int hashCode() + { + int hashCode = getNodeRef().hashCode(); + if (getPermissionReference() != null) + { + hashCode = hashCode * 37 + getPermissionReference().hashCode(); + } + if (getAuthority() != null) + { + hashCode = hashCode * 37 + getAuthority().hashCode(); + } + if(getAccessStatus() != null) + { + hashCode = hashCode * 37 + getAccessStatus().hashCode(); + } + return hashCode; + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionReference.java b/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionReference.java new file mode 100644 index 0000000000..77f54fe684 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionReference.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.repo.security.permissions.PermissionReference; + + +/** + * This class provides common support for hash code and equality. + * + * @author andyh + */ +public abstract class AbstractPermissionReference implements PermissionReference +{ + + public AbstractPermissionReference() + { + super(); + } + + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(!(o instanceof AbstractPermissionReference)) + { + return false; + } + AbstractPermissionReference other = (AbstractPermissionReference)o; + return this.getName().equals(other.getName()) && this.getQName().equals(other.getQName()); + } + + @Override + public int hashCode() + { + return getQName().hashCode() * 37 + getName().hashCode(); + } + + @Override + public String toString() + { + return getQName()+ "." + getName(); + } + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java b/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java new file mode 100644 index 0000000000..a029b15605 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.MutableAuthenticationDao; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.PermissionServiceSPI; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.springframework.orm.hibernate3.LocalSessionFactoryBean; + +public class AbstractPermissionTest extends BaseSpringTest +{ + + protected static final String ROLE_AUTHENTICATED = "ROLE_AUTHENTICATED"; + + protected NodeService nodeService; + + protected DictionaryService dictionaryService; + + protected PermissionServiceSPI permissionService; + + protected AuthenticationService authenticationService; + + private MutableAuthenticationDao authenticationDAO; + + protected LocalSessionFactoryBean sessionFactory; + + protected NodeRef rootNodeRef; + + protected NamespacePrefixResolver namespacePrefixResolver; + + protected ServiceRegistry serviceRegistry; + + protected NodeRef systemNodeRef; + + protected AuthenticationComponent authenticationComponent; + + protected ModelDAO permissionModelDAO; + + protected PersonService personService; + + protected AuthorityService authorityService; + + public AbstractPermissionTest() + { + super(); + // TODO Auto-generated constructor stub + } + + protected void onSetUpInTransaction() throws Exception + { + nodeService = (NodeService) applicationContext.getBean("nodeService"); + dictionaryService = (DictionaryService) applicationContext.getBean(ServiceRegistry.DICTIONARY_SERVICE + .getLocalName()); + permissionService = (PermissionServiceSPI) applicationContext.getBean("permissionService"); + namespacePrefixResolver = (NamespacePrefixResolver) applicationContext + .getBean(ServiceRegistry.NAMESPACE_SERVICE.getLocalName()); + authenticationService = (AuthenticationService) applicationContext.getBean("authenticationService"); + authenticationComponent = (AuthenticationComponent) applicationContext.getBean("authenticationComponent"); + serviceRegistry = (ServiceRegistry) applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + permissionModelDAO = (ModelDAO) applicationContext.getBean("permissionsModelDAO"); + personService = (PersonService) applicationContext.getBean("personService"); + authorityService = (AuthorityService) applicationContext.getBean("authorityService"); + + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + authenticationDAO = (MutableAuthenticationDao) applicationContext.getBean("alfDaoImpl"); + + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.nanoTime()); + rootNodeRef = nodeService.getRootNode(storeRef); + + QName children = ContentModel.ASSOC_CHILDREN; + QName system = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "system"); + QName container = ContentModel.TYPE_CONTAINER; + QName types = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "people"); + + systemNodeRef = nodeService.createNode(rootNodeRef, children, system, container).getChildRef(); + NodeRef typesNodeRef = nodeService.createNode(systemNodeRef, children, types, container).getChildRef(); + Map props = createPersonProperties("andy"); + nodeService.createNode(typesNodeRef, children, ContentModel.TYPE_PERSON, container, props).getChildRef(); + props = createPersonProperties("lemur"); + nodeService.createNode(typesNodeRef, children, ContentModel.TYPE_PERSON, container, props).getChildRef(); + + // create an authentication object e.g. the user + if(authenticationDAO.userExists("andy")) + { + authenticationService.deleteAuthentication("andy"); + } + authenticationService.createAuthentication("andy", "andy".toCharArray()); + + if(authenticationDAO.userExists("lemur")) + { + authenticationService.deleteAuthentication("lemur"); + } + authenticationService.createAuthentication("lemur", "lemur".toCharArray()); + + if(authenticationDAO.userExists("admin")) + { + authenticationService.deleteAuthentication("admin"); + } + authenticationService.createAuthentication("admin", "admin".toCharArray()); + + authenticationComponent.clearCurrentSecurityContext(); + } + + protected void onTearDownInTransaction() + { + flushAndClear(); + super.onTearDownInTransaction(); + } + + protected void runAs(String userName) + { + authenticationService.authenticate(userName, userName.toCharArray()); + assertNotNull(authenticationService.getCurrentUserName()); + // for(GrantedAuthority authority : woof.getAuthorities()) + // { + // System.out.println("Auth = "+authority.getAuthority()); + // } + + } + + private Map createPersonProperties(String userName) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + return properties; + } + + protected PermissionReference getPermission(String permission) + { + return permissionModelDAO.getPermissionReference(null, permission); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/AlwaysProceedMethodInterceptor.java b/source/java/org/alfresco/repo/security/permissions/impl/AlwaysProceedMethodInterceptor.java new file mode 100644 index 0000000000..ac7ee7ba7d --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/AlwaysProceedMethodInterceptor.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +public class AlwaysProceedMethodInterceptor implements MethodInterceptor +{ + + public AlwaysProceedMethodInterceptor() + { + super(); + } + + public Object invoke(MethodInvocation mi) throws Throwable + { + return mi.proceed(); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/ExceptionTranslatorMethodInterceptor.java b/source/java/org/alfresco/repo/security/permissions/impl/ExceptionTranslatorMethodInterceptor.java new file mode 100644 index 0000000000..e8d1225381 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/ExceptionTranslatorMethodInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import net.sf.acegisecurity.AccessDeniedException; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +public class ExceptionTranslatorMethodInterceptor implements MethodInterceptor +{ + private static final String MSG_ACCESS_DENIED = "permissions.err_access_denied"; + + public ExceptionTranslatorMethodInterceptor() + { + super(); + } + + public Object invoke(MethodInvocation mi) throws Throwable + { + try + { + return mi.proceed(); + } + catch(AccessDeniedException ade) + { + throw new org.alfresco.repo.security.permissions.AccessDeniedException(MSG_ACCESS_DENIED, ade); + } + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/ModelDAO.java b/source/java/org/alfresco/repo/security/permissions/impl/ModelDAO.java new file mode 100644 index 0000000000..218958c8bf --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/ModelDAO.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import java.util.Set; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * The API for the alfresco permission model. + * + * @author Andy Hind + */ +public interface ModelDAO +{ + + /** + * Get the permissions that can be set for the given type. + * + * @param type - the type in the data dictionary. + * @return + */ + public Set getAllPermissions(QName type); + + /** + * Get the permissions that can be set for the given node. + * This is determined by the node type. + * + * @param nodeRef + * @return + */ + public Set getAllPermissions(NodeRef nodeRef); + + /** + *Get the permissions that are exposed to be set for the given type. + * + * @param type - the type in the data dictionary. + * @return + */ + public Set getExposedPermissions(QName type); + + /** + * Get the permissions that are exposed to be set for the given node. + * This is determined by the node type. + * + * @param nodeRef + * @return + */ + public Set getExposedPermissions(NodeRef nodeRef); + + /** + * Get all the permissions that grant this permission. + * + * @param perm + * @return + */ + public Set getGrantingPermissions(PermissionReference perm); + + /** + * Get the permissions that must also be present on the node for the required permission to apply. + * + * @param required + * @param qName + * @param aspectQNames + * @param on + * @return + */ + public Set getRequiredPermissions(PermissionReference required, QName qName, Set aspectQNames, RequiredPermission.On on); + + /** + * Get the permissions which are granted by the supplied permission. + * + * @param permissionReference + * @return + */ + public Set getGranteePermissions(PermissionReference permissionReference); + + /** + * Is this permission refernece to a permission and not a permissoinSet? + * + * @param required + * @return + */ + public boolean checkPermission(PermissionReference required); + + /** + * Does the permission reference have a unique name? + * + * @param permissionReference + * @return + */ + public boolean isUnique(PermissionReference permissionReference); + + /** + * Find a permission by name in the type context. + * If the context is null and the permission name is unique it will be found. + * + * @param qname + * @param permissionName + * @return + */ + public PermissionReference getPermissionReference(QName qname, String permissionName); + + /** + * Get the global permissions for the model. + * Permissions that apply to all nodes and take precedence over node specific permissions. + * + * @return + */ + public Set getGlobalPermissionEntries(); + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/PermissionReferenceImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/PermissionReferenceImpl.java new file mode 100644 index 0000000000..a65dd93707 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/PermissionReferenceImpl.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.service.namespace.QName; + +/** + * A simple permission reference (not persisted). + * + * A permission is identified by name for a given type, which is identified by its qualified name. + * + * @author andyh + */ +public class PermissionReferenceImpl extends AbstractPermissionReference +{ + private QName qName; + + private String name; + + public PermissionReferenceImpl(QName qName, String name) + { + this.qName = qName; + this.name = name; + } + + public String getName() + { + return name; + } + + public QName getQName() + { + return qName; + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java new file mode 100644 index 0000000000..d6a0f8872a --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java @@ -0,0 +1,1042 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.providers.dao.User; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.permissions.DynamicAuthority; +import org.alfresco.repo.security.permissions.NodePermissionEntry; +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.PermissionServiceSPI; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * The Alfresco implementation of a permissions service against our APIs for the + * permissions model and permissions persistence. + * + * + * @author andyh + */ +public class PermissionServiceImpl implements PermissionServiceSPI, InitializingBean +{ + + static SimplePermissionReference OLD_ALL_PERMISSIONS_REFERENCE = new SimplePermissionReference(QName.createQName( + NamespaceService.SECURITY_MODEL_1_0_URI, PermissionService.ALL_PERMISSIONS), PermissionService.ALL_PERMISSIONS); + + + private static Log log = LogFactory.getLog(PermissionServiceImpl.class); + + /* + * Access to the model + */ + private ModelDAO modelDAO; + + /* + * Access to permissions + */ + private PermissionsDAO permissionsDAO; + + /* + * Access to the node service + */ + private NodeService nodeService; + + /* + * Access to the data dictionary + */ + private DictionaryService dictionaryService; + + /* + * Access to the authentication component + */ + + private AuthenticationComponent authenticationComponent; + + private AuthorityService authorityService; + + /* + * Dynamic authorities providers + */ + + private List dynamicAuthorities; + + /* + * Standard spring construction. + */ + public PermissionServiceImpl() + { + super(); + } + + // + // Inversion of control + // + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setModelDAO(ModelDAO modelDAO) + { + this.modelDAO = modelDAO; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPermissionsDAO(PermissionsDAO permissionsDAO) + { + this.permissionsDAO = permissionsDAO; + } + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setDynamicAuthorities(List dynamicAuthorities) + { + this.dynamicAuthorities = dynamicAuthorities; + } + + public void afterPropertiesSet() throws Exception + { + if (dictionaryService == null) + { + throw new IllegalArgumentException("There must be a dictionary service"); + } + if (modelDAO == null) + { + throw new IllegalArgumentException("There must be a permission model service"); + } + if (nodeService == null) + { + throw new IllegalArgumentException("There must be a node service"); + } + if (permissionsDAO == null) + { + throw new IllegalArgumentException("There must be a permission dao"); + } + if (authenticationComponent == null) + { + throw new IllegalArgumentException("There must be an authentication component"); + } + if(authorityService == null) + { + throw new IllegalArgumentException("There must be an authority service"); + } + + } + + // + // Permissions Service + // + + public String getOwnerAuthority() + { + return OWNER_AUTHORITY; + } + + public String getAllAuthorities() + { + return ALL_AUTHORITIES; + } + + public String getAllPermission() + { + return ALL_PERMISSIONS; + } + + public Set getPermissions(NodeRef nodeRef) + { + return getAllPermissionsImpl(nodeRef, true, true); + } + + public Set getAllSetPermissions(NodeRef nodeRef) + { + HashSet accessPermissions = new HashSet(); + NodePermissionEntry nodePremissionEntry = getSetPermissions(nodeRef); + for (PermissionEntry pe : nodePremissionEntry.getPermissionEntries()) + { + accessPermissions.add(new AccessPermissionImpl(getPermission(pe.getPermissionReference()), pe + .getAccessStatus(), pe.getAuthority())); + } + return accessPermissions; + } + + private Set getAllPermissionsImpl(NodeRef nodeRef, boolean includeTrue, boolean includeFalse) + { + String userName = authenticationComponent.getCurrentUserName(); + HashSet accessPermissions = new HashSet(); + for (PermissionReference pr : getSettablePermissionReferences(nodeRef)) + { + if (hasPermission(nodeRef, pr) == AccessStatus.ALLOWED) + { + accessPermissions.add(new AccessPermissionImpl(getPermission(pr), AccessStatus.ALLOWED, userName)); + } + else + { + if (includeFalse) + { + accessPermissions.add(new AccessPermissionImpl(getPermission(pr), AccessStatus.DENIED, userName)); + } + } + } + return accessPermissions; + } + + private class AccessPermissionImpl implements AccessPermission + { + private String permission; + + private AccessStatus accessStatus; + + private String authority; + + private AuthorityType authorityType; + + AccessPermissionImpl(String permission, AccessStatus accessStatus, String authority) + { + this.permission = permission; + this.accessStatus = accessStatus; + this.authority = authority; + this.authorityType = AuthorityType.getAuthorityType(authority); + } + + public String getPermission() + { + return permission; + } + + public AccessStatus getAccessStatus() + { + return accessStatus; + } + + public String getAuthority() + { + return authority; + } + + public AuthorityType getAuthorityType() + { + return authorityType; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof AccessPermissionImpl)) + { + return false; + } + AccessPermissionImpl other = (AccessPermissionImpl) o; + return this.getPermission().equals(other.getPermission()) + && (this.getAccessStatus() == other.getAccessStatus() && (this.getAccessStatus().equals(other + .getAccessStatus()))); + } + + @Override + public int hashCode() + { + return ((authority.hashCode() * 37) + permission.hashCode()) * 37 + accessStatus.hashCode(); + } + } + + public Set getSettablePermissions(NodeRef nodeRef) + { + Set settable = getSettablePermissionReferences(nodeRef); + Set strings = new HashSet(settable.size()); + for (PermissionReference pr : settable) + { + strings.add(getPermission(pr)); + } + return strings; + } + + public Set getSettablePermissions(QName type) + { + Set settable = getSettablePermissionReferences(type); + Set strings = new HashSet(settable.size()); + for (PermissionReference pr : settable) + { + strings.add(getPermission(pr)); + } + return strings; + } + + public NodePermissionEntry getSetPermissions(NodeRef nodeRef) + { + return permissionsDAO.getPermissions(nodeRef); + } + + public AccessStatus hasPermission(NodeRef nodeRef, PermissionReference perm) + { + // If the node ref is null there is no sensible test to do - and there + // must be no permissions + // - so we allow it + + if (nodeRef == null) + { + return AccessStatus.ALLOWED; + } + + // If the permission is null we deny + + if (perm == null) + { + return AccessStatus.DENIED; + } + + // Allow permissions for nodes that do not exist + if (!nodeService.exists(nodeRef)) + { + return AccessStatus.ALLOWED; + } + + // If the node does not support the given permission there is no point + // doing the test + Set available = modelDAO.getAllPermissions(nodeRef); + available.add(getAllPermissionReference()); + available.add(OLD_ALL_PERMISSIONS_REFERENCE); + + if (!(available.contains(perm))) + { + return AccessStatus.DENIED; + } + + // + // TODO: Dynamic permissions via evaluators + // + + /* + * Does the current authentication have the supplied permission on the + * given node. + */ + + // Get the current authentications + Authentication auth = authenticationComponent.getCurrentAuthentication(); + + // Get the available authorisations + Set authorisations = getAuthorisations(auth, nodeRef); + + QName typeQname = nodeService.getType(nodeRef); + Set aspectQNames = nodeService.getAspects(nodeRef); + + NodeTest nt = new NodeTest(perm.equals(OLD_ALL_PERMISSIONS_REFERENCE) ? getAllPermissionReference() : perm, typeQname, aspectQNames); + boolean result = nt.evaluate(authorisations, nodeRef); + if (log.isDebugEnabled()) + { + log.debug("Permission <" + + perm + "> is " + (result ? "allowed" : "denied") + " for " + + authenticationComponent.getCurrentUserName() + " on node " + nodeService.getPath(nodeRef)); + } + return result ? AccessStatus.ALLOWED : AccessStatus.DENIED; + + } + + /** + * Get the authorisations for the currently authenticated user + * + * @param auth + * @return + */ + private Set getAuthorisations(Authentication auth, NodeRef nodeRef) + { + HashSet auths = new HashSet(); + // No authenticated user then no permissions + if (auth == null) + { + return auths; + } + // TODO: Refactor and use the authentication service for this. + User user = (User) auth.getPrincipal(); + auths.add(user.getUsername()); + auths.add(getAllAuthorities()); + for (GrantedAuthority authority : auth.getAuthorities()) + { + auths.add(authority.getAuthority()); + } + if (dynamicAuthorities != null) + { + for (DynamicAuthority da : dynamicAuthorities) + { + if (da.hasAuthority(nodeRef, user.getUsername())) + { + auths.add(da.getAuthority()); + } + } + } + auths.addAll(authorityService.getAuthorities()); + return auths; + } + + public NodePermissionEntry explainPermission(NodeRef nodeRef, PermissionReference perm) + { + // TODO Auto-generated method stub + return null; + } + + public void deletePermissions(NodeRef nodeRef) + { + permissionsDAO.deletePermissions(nodeRef); + } + + public void deletePermissions(NodePermissionEntry nodePermissionEntry) + { + permissionsDAO.deletePermissions(nodePermissionEntry); + } + + public void deletePermission(PermissionEntry permissionEntry) + { + permissionsDAO.deletePermissions(permissionEntry); + } + + public void deletePermission(NodeRef nodeRef, String authority, PermissionReference perm, boolean allow) + { + permissionsDAO.deletePermissions(nodeRef, authority, perm, allow); + } + + public void clearPermission(NodeRef nodeRef, String authority) + { + permissionsDAO.clearPermission(nodeRef, authority); + } + + public void setPermission(NodeRef nodeRef, String authority, PermissionReference perm, boolean allow) + { + permissionsDAO.setPermission(nodeRef, authority, perm, allow); + } + + public void setPermission(PermissionEntry permissionEntry) + { + permissionsDAO.setPermission(permissionEntry); + } + + public void setPermission(NodePermissionEntry nodePermissionEntry) + { + permissionsDAO.setPermission(nodePermissionEntry); + } + + public void setInheritParentPermissions(NodeRef nodeRef, boolean inheritParentPermissions) + { + permissionsDAO.setInheritParentPermissions(nodeRef, inheritParentPermissions); + } + + /** + * @see org.alfresco.service.cmr.security.PermissionService#getInheritParentPermissions(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean getInheritParentPermissions(NodeRef nodeRef) + { + return permissionsDAO.getInheritParentPermissions(nodeRef); + } + + // + // SUPPORT CLASSES + // + + /** + * Support class to test the permission on a node. + * + * @author Andy Hind + */ + private class NodeTest + { + /* + * The required permission. + */ + PermissionReference required; + + /* + * Granters of the permission + */ + Set granters; + + /* + * The additional permissions required at the node level. + */ + Set nodeRequirements = new HashSet(); + + /* + * The additional permissions required on the parent. + */ + Set parentRequirements = new HashSet(); + + /* + * The permissions required on all children . + */ + Set childrenRequirements = new HashSet(); + + /* + * The type name of the node. + */ + QName typeQName; + + /* + * The aspects set on the node. + */ + Set aspectQNames; + + /* + * Constructor just gets the additional requirements + */ + NodeTest(PermissionReference required, QName typeQName, Set aspectQNames) + { + this.required = required; + this.typeQName = typeQName; + this.aspectQNames = aspectQNames; + + // Set the required node permissions + nodeRequirements = modelDAO.getRequiredPermissions(required, typeQName, aspectQNames, + RequiredPermission.On.NODE); + + parentRequirements = modelDAO.getRequiredPermissions(required, typeQName, aspectQNames, + RequiredPermission.On.PARENT); + + childrenRequirements = modelDAO.getRequiredPermissions(required, typeQName, aspectQNames, + RequiredPermission.On.CHILDREN); + + // Find all the permissions that grant the allowed permission + // All permissions are treated specially. + granters = modelDAO.getGrantingPermissions(required); + granters.add(getAllPermissionReference()); + granters.add(OLD_ALL_PERMISSIONS_REFERENCE); + } + + /** + * External hook point + * + * @param authorisations + * @param nodeRef + * @return + */ + boolean evaluate(Set authorisations, NodeRef nodeRef) + { + Set> denied = new HashSet>(); + return evaluate(authorisations, nodeRef, denied, null); + } + + /** + * Internal hook point for recursion + * + * @param authorisations + * @param nodeRef + * @param denied + * @param recursiveIn + * @return + */ + boolean evaluate(Set authorisations, NodeRef nodeRef, Set> denied, + MutableBoolean recursiveIn) + { + // Do we defer our required test to a parent (yes if not null) + MutableBoolean recursiveOut = null; + + Set> locallyDenied = new HashSet>(); + locallyDenied.addAll(denied); + locallyDenied.addAll(getDenied(nodeRef)); + + // Start out true and "and" all other results + boolean success = true; + + // Check the required permissions but not for sets they rely on + // their underlying permissions + if (required.equals(getPermissionReference(ALL_PERMISSIONS)) || modelDAO.checkPermission(required)) + { + if (parentRequirements.contains(required)) + { + if (checkGlobalPermissions(authorisations) || checkRequired(authorisations, nodeRef, locallyDenied)) + { + // No need to do the recursive test as it has been found + recursiveOut = null; + if (recursiveIn != null) + { + recursiveIn.setValue(true); + } + } + else + { + // Much cheaper to do this as we go then check all the + // stack values for each parent + recursiveOut = new MutableBoolean(false); + } + } + else + { + // We have to do the test as no parent will help us out + success &= hasSinglePermission(authorisations, nodeRef); + } + if (!success) + { + return false; + } + } + + // Check the other permissions required on the node + for (PermissionReference pr : nodeRequirements) + { + // Build a new test + NodeTest nt = new NodeTest(pr, typeQName, aspectQNames); + success &= nt.evaluate(authorisations, nodeRef, locallyDenied, null); + if (!success) + { + return false; + } + } + + // Check the permission required of the parent + + if (success) + { + ChildAssociationRef car = nodeService.getPrimaryParent(nodeRef); + if (car.getParentRef() != null) + { + + NodePermissionEntry nodePermissions = permissionsDAO.getPermissions(car.getChildRef()); + if ((nodePermissions == null) || (nodePermissions.inheritPermissions())) + { + + locallyDenied.addAll(getDenied(car.getParentRef())); + for (PermissionReference pr : parentRequirements) + { + if (pr.equals(required)) + { + // Recursive permission + success &= this.evaluate(authorisations, car.getParentRef(), locallyDenied, + recursiveOut); + if ((recursiveOut != null) && recursiveOut.getValue()) + { + if (recursiveIn != null) + { + recursiveIn.setValue(true); + } + } + } + else + { + NodeTest nt = new NodeTest(pr, typeQName, aspectQNames); + success &= nt.evaluate(authorisations, car.getParentRef(), locallyDenied, null); + } + + if (!success) + { + return false; + } + } + } + } + } + + if ((recursiveOut != null) && (!recursiveOut.getValue())) + { + // The required authentication was not resolved in recursion + return false; + } + + // Check permissions required of children + if (childrenRequirements.size() > 0) + { + List childAssocRefs = nodeService.getChildAssocs(nodeRef); + for (PermissionReference pr : childrenRequirements) + { + for (ChildAssociationRef child : childAssocRefs) + { + success &= (hasPermission(child.getChildRef(), pr) == AccessStatus.ALLOWED); + if (!success) + { + return false; + } + } + } + } + + return success; + } + + public boolean hasSinglePermission(Set authorisations, NodeRef nodeRef) + { + // Check global permission + + if (checkGlobalPermissions(authorisations)) + { + return true; + } + + Set> denied = new HashSet>(); + + // Keep track of permission that are denied + + // Permissions are only evaluated up the primary parent chain + // TODO: Do not ignore non primary permissions + ChildAssociationRef car = nodeService.getPrimaryParent(nodeRef); + // Work up the parent chain evaluating permissions. + while (car != null) + { + // Add any denied permission to the denied list - these can not + // then + // be used to given authentication. + // A -> B -> C + // If B denies all permissions to any - allowing all permissions + // to + // andy at node A has no effect + + denied.addAll(getDenied(car.getChildRef())); + + // If the current node allows the permission we are done + // The test includes any parent or ancestor requirements + if (checkRequired(authorisations, car.getChildRef(), denied)) + { + return true; + } + + // Build the next element of the evaluation chain + if (car.getParentRef() != null) + { + NodePermissionEntry nodePermissions = permissionsDAO.getPermissions(car.getChildRef()); + if ((nodePermissions == null) || (nodePermissions.inheritPermissions())) + { + car = nodeService.getPrimaryParent(car.getParentRef()); + } + else + { + car = null; + } + } + else + { + car = null; + } + + } + + // TODO: Support meta data permissions on the root node? + + return false; + + } + + /** + * Check if we have a global permission + * + * @param authorisations + * @return + */ + private boolean checkGlobalPermissions(Set authorisations) + { + for (PermissionEntry pe : modelDAO.getGlobalPermissionEntries()) + { + if (isGranted(pe, authorisations, null)) + { + return true; + } + } + return false; + } + + /** + * Get the list of permissions denied for this node. + * + * @param nodeRef + * @return + */ + Set> getDenied(NodeRef nodeRef) + { + Set> deniedSet = new HashSet>(); + + // Loop over all denied permissions + NodePermissionEntry nodeEntry = permissionsDAO.getPermissions(nodeRef); + if (nodeEntry != null) + { + for (PermissionEntry pe : nodeEntry.getPermissionEntries()) + { + if (pe.isDenied()) + { + // All the sets that grant this permission must be + // denied + // Note that granters includes the orginal permission + Set granters = modelDAO + .getGrantingPermissions(pe.getPermissionReference()); + for (PermissionReference granter : granters) + { + deniedSet.add(new Pair(pe.getAuthority(), granter)); + } + + // All the things granted by this permission must be + // denied + Set grantees = modelDAO.getGranteePermissions(pe.getPermissionReference()); + for (PermissionReference grantee : grantees) + { + deniedSet.add(new Pair(pe.getAuthority(), grantee)); + } + + // All permission excludes all permissions available for + // the node. + if (pe.getPermissionReference().equals(getAllPermissionReference()) || pe.getPermissionReference().equals(OLD_ALL_PERMISSIONS_REFERENCE)) + { + for (PermissionReference deny : modelDAO.getAllPermissions(nodeRef)) + { + deniedSet.add(new Pair(pe.getAuthority(), deny)); + } + } + } + } + } + return deniedSet; + } + + /** + * Check that a given authentication is available on a node + * + * @param authorisations + * @param nodeRef + * @param denied + * @return + */ + boolean checkRequired(Set authorisations, NodeRef nodeRef, Set> denied) + { + NodePermissionEntry nodeEntry = permissionsDAO.getPermissions(nodeRef); + + // No permissions set - short cut to deny + if (nodeEntry == null) + { + return false; + } + + // Check if each permission allows - the first wins. + // We could have other voting style mechanisms here + for (PermissionEntry pe : nodeEntry.getPermissionEntries()) + { + if (isGranted(pe, authorisations, denied)) + { + return true; + } + } + return false; + } + + /** + * Is a permission granted + * + * @param pe - + * the permissions entry to consider + * @param granters - + * the set of granters + * @param authorisations - + * the set of authorities + * @param denied - + * the set of denied permissions/authority pais + * @return + */ + private boolean isGranted(PermissionEntry pe, Set authorisations, + Set> denied) + { + // If the permission entry denies then we just deny + if (pe.isDenied()) + { + return false; + } + + // The permission is allowed but we deny it as it is in the denied + // set + if (denied != null) + { + Pair specific = new Pair(pe.getAuthority(), + required); + if (denied.contains(specific)) + { + return false; + } + } + + // If the permission has a match in both the authorities and + // granters list it is allowed + // It applies to the current user and it is granted + if (authorisations.contains(pe.getAuthority()) && granters.contains(pe.getPermissionReference())) + { + { + return true; + } + } + + // Default deny + return false; + + } + } + + /** + * Helper class to store a pair of objects which may be null + * + * @author Andy Hind + */ + private static class Pair + { + A a; + + B b; + + Pair(A a, B b) + { + this.a = a; + this.b = b; + } + + A getA() + { + return a; + } + + B getB() + { + return b; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(this instanceof Pair)) + { + return false; + } + Pair other = (Pair) o; + return EqualsHelper.nullSafeEquals(this.getA(), other.getA()) + && EqualsHelper.nullSafeEquals(this.getB(), other.getB()); + } + + @Override + public int hashCode() + { + return (((a == null) ? 0 : a.hashCode()) * 37) + ((b == null) ? 0 : b.hashCode()); + } + + } + + private static class MutableBoolean + { + private boolean value; + + MutableBoolean(boolean value) + { + this.value = value; + } + + void setValue(boolean value) + { + this.value = value; + } + + boolean getValue() + { + return value; + } + } + + public PermissionReference getPermissionReference(QName qname, String permissionName) + { + return modelDAO.getPermissionReference(qname, permissionName); + } + + public PermissionReference getAllPermissionReference() + { + return getPermissionReference(ALL_PERMISSIONS); + } + + + + public String getPermission(PermissionReference permissionReference) + { + if (modelDAO.isUnique(permissionReference)) + { + return permissionReference.getName(); + } + else + { + return permissionReference.toString(); + } + } + + public PermissionReference getPermissionReference(String permissionName) + { + return modelDAO.getPermissionReference(null, permissionName); + } + + public Set getSettablePermissionReferences(QName type) + { + return modelDAO.getExposedPermissions(type); + } + + public Set getSettablePermissionReferences(NodeRef nodeRef) + { + return modelDAO.getExposedPermissions(nodeRef); + } + + public void deletePermission(NodeRef nodeRef, String authority, String perm, boolean allow) + { + deletePermission(nodeRef, authority, getPermissionReference(perm), allow); + } + + public AccessStatus hasPermission(NodeRef nodeRef, String perm) + { + return hasPermission(nodeRef, getPermissionReference(perm)); + } + + public void setPermission(NodeRef nodeRef, String authority, String perm, boolean allow) + { + setPermission(nodeRef, authority, getPermissionReference(perm), allow); + } + + public void deletePermissions(String recipient) + { + permissionsDAO.deleteAllPermissionsForAuthority(recipient); + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java new file mode 100644 index 0000000000..52a9234913 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java @@ -0,0 +1,1909 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import java.util.HashSet; +import java.util.Set; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.GrantedAuthority; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; + +public class PermissionServiceTest extends AbstractPermissionTest +{ + public PermissionServiceTest() + { + super(); + // TODO Auto-generated constructor stub + } + + public void testAuthenticatedRoleIsPresent() + { + runAs("andy"); + Authentication auth = authenticationComponent.getCurrentAuthentication(); + for (GrantedAuthority authority : auth.getAuthorities()) + { + if (authority.getAuthority().equals(ROLE_AUTHENTICATED)) + { + return; + } + } + fail("Missing role ROLE_AUTHENTICATED "); + } + + + + public void testSetInheritFalse() + { + runAs("andy"); + permissionService.setInheritParentPermissions(rootNodeRef, false); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertFalse(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(0, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + } + + public void testSetInheritTrue() + { + runAs("andy"); + permissionService.setInheritParentPermissions(rootNodeRef, true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(0, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermissions(permissionService.getSetPermissions(rootNodeRef)); + } + + public void testAlterInherit() + { + runAs("andy"); + testSetInheritFalse(); + testSetInheritTrue(); + testSetInheritFalse(); + testSetInheritTrue(); + + permissionService.deletePermissions(rootNodeRef); + // testUnset(); + } + + public void testSetNodePermissionEntry() + { + runAs("andy"); + Set entries = new HashSet(); + entries.add(new SimplePermissionEntry(rootNodeRef, new SimplePermissionReference(QName.createQName("A", "B"), + "C"), "user-one", AccessStatus.ALLOWED)); + entries.add(new SimplePermissionEntry(rootNodeRef, permissionService.getAllPermissionReference(), "user-two", + AccessStatus.ALLOWED)); + entries.add(new SimplePermissionEntry(rootNodeRef, new SimplePermissionReference(QName.createQName("D", "E"), + "F"), permissionService.getAllAuthorities(), AccessStatus.ALLOWED)); + entries.add(new SimplePermissionEntry(rootNodeRef, permissionService.getAllPermissionReference(), + permissionService.getAllAuthorities(), AccessStatus.DENIED)); + + SimpleNodePermissionEntry entry = new SimpleNodePermissionEntry(rootNodeRef, false, entries); + + permissionService.setPermission(entry); + + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertFalse(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(4, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + } + + public void testSetNodePermissionEntry2() + { + Set entries = new HashSet(); + entries.add(new SimplePermissionEntry(rootNodeRef, permissionService.getAllPermissionReference(), + permissionService.getAllAuthorities(), AccessStatus.ALLOWED)); + + SimpleNodePermissionEntry entry = new SimpleNodePermissionEntry(rootNodeRef, false, entries); + + permissionService.setPermission(entry); + + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertFalse(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + } + + public void testAlterNodePermissions() + { + testSetNodePermissionEntry(); + testSetNodePermissionEntry2(); + testSetNodePermissionEntry(); + testSetNodePermissionEntry2(); + } + + public void testSetPermissionEntryElements() + { + permissionService.setPermission(rootNodeRef, "andy", permissionService.getAllPermission(), true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + for (PermissionEntry pe : permissionService.getSetPermissions(rootNodeRef).getPermissionEntries()) + { + assertEquals("andy", pe.getAuthority()); + assertTrue(pe.isAllowed()); + assertTrue(pe.getPermissionReference().getQName().equals( + permissionService.getAllPermissionReference().getQName())); + assertTrue(pe.getPermissionReference().getName().equals( + permissionService.getAllPermissionReference().getName())); + assertEquals(rootNodeRef, pe.getNodeRef()); + } + + // Set duplicate + + permissionService.setPermission(rootNodeRef, "andy", permissionService.getAllPermission(), true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // Set new + + permissionService.setPermission(rootNodeRef, "other", permissionService.getAllPermission(), true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(2, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // Add deny + + permissionService.setPermission(rootNodeRef, "andy", permissionService.getAllPermission(), false); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(3, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // new + + permissionService.setPermission(rootNodeRef, "andy", PermissionService.READ, false); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(4, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // delete + + permissionService.deletePermission(rootNodeRef, "andy", PermissionService.READ, false); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(3, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(rootNodeRef, "andy", permissionService.getAllPermission(), false); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(2, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(rootNodeRef, "other", permissionService.getAllPermission(), true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(rootNodeRef, "andy", permissionService.getAllPermission(), true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(0, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + } + + public void testSetPermissionEntry() + { + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(rootNodeRef, "andy", permissionService.getAllPermission(), true); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + for (PermissionEntry pe : permissionService.getSetPermissions(rootNodeRef).getPermissionEntries()) + { + assertEquals("andy", pe.getAuthority()); + assertTrue(pe.isAllowed()); + assertTrue(pe.getPermissionReference().getQName().equals( + permissionService.getAllPermissionReference().getQName())); + assertTrue(pe.getPermissionReference().getName().equals( + permissionService.getAllPermissionReference().getName())); + assertEquals(rootNodeRef, pe.getNodeRef()); + } + + // Set duplicate + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.ALLOWED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // Set new + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "other", AccessStatus.ALLOWED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(2, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // Deny + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.DENIED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(3, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + // new + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, new SimplePermissionReference(QName + .createQName("A", "B"), "C"), "andy", AccessStatus.DENIED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(4, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, new SimplePermissionReference(QName + .createQName("A", "B"), "C"), "andy", AccessStatus.DENIED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(3, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.DENIED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(2, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "other", AccessStatus.ALLOWED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(1, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.ALLOWED)); + assertNotNull(permissionService.getSetPermissions(rootNodeRef)); + assertTrue(permissionService.getSetPermissions(rootNodeRef).inheritPermissions()); + assertEquals(rootNodeRef, permissionService.getSetPermissions(rootNodeRef).getNodeRef()); + assertEquals(0, permissionService.getSetPermissions(rootNodeRef).getPermissionEntries().size()); + } + + public void testGetSettablePermissionsForType() + { + Set answer = permissionService.getSettablePermissions(QName.createQName("sys", "base", + namespacePrefixResolver)); + assertEquals(17, answer.size()); + + answer = permissionService.getSettablePermissions(QName.createQName("cm", "ownable", namespacePrefixResolver)); + assertEquals(0, answer.size()); + + answer = permissionService.getSettablePermissions(QName.createQName("cm", "content", namespacePrefixResolver)); + assertEquals(21, answer.size()); + + answer = permissionService.getSettablePermissions(QName.createQName("cm", "folder", namespacePrefixResolver)); + assertEquals(4, answer.size()); + } + + public void testGetSettablePermissionsForNode() + { + QName ownable = QName.createQName("cm", "ownable", namespacePrefixResolver); + + Set answer = permissionService.getSettablePermissions(rootNodeRef); + assertEquals(21, answer.size()); + + nodeService.addAspect(rootNodeRef, ownable, null); + answer = permissionService.getSettablePermissions(rootNodeRef); + assertEquals(21, answer.size()); + + nodeService.removeAspect(rootNodeRef, ownable); + answer = permissionService.getSettablePermissions(rootNodeRef); + assertEquals(21, answer.size()); + } + + public void testSimplePermissionOnRoot() + { + runAs("andy"); + + assertEquals(21, permissionService.getPermissions(rootNodeRef).size()); + assertEquals(0, countGranted(permissionService.getPermissions(rootNodeRef))); + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + + assertEquals(21, permissionService.getPermissions(rootNodeRef).size()); + assertEquals(1, countGranted(permissionService.getPermissions(rootNodeRef))); + + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + } + + private int countGranted(Set permissions) + { + int count = 0; + for (AccessPermission ap : permissions) + { + if (ap.getAccessStatus() == AccessStatus.ALLOWED) + { + count++; + } + } + return count; + } + + public void testGlobalPermissionsForAdmin() + { + runAs("admin"); + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + NodeRef n2 = nodeService.createNode(n1, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}two"), + ContentModel.TYPE_CONTENT).getChildRef(); + + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "admin", AccessStatus.DENIED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "admin", AccessStatus.DENIED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "admin", AccessStatus.DENIED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CONTENT), "admin", AccessStatus.DENIED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.ALL_PERMISSIONS), "admin", AccessStatus.DENIED)); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testPermissionGroupOnRoot() + { + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + + assertEquals(21, permissionService.getPermissions(rootNodeRef).size()); + assertEquals(3, countGranted(permissionService.getPermissions(rootNodeRef))); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("andy"); + } + + public void testSimplePermissionSimpleInheritance() + { + runAs("admin"); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + assertEquals(3, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(3, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + assertEquals(3, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(3, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + } + + public void testPermissionGroupSimpleInheritance() + { + runAs("admin"); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n1, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testDenySimplePermisionOnRootNode() + { + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + } + + public void testDenyPermissionOnRootNOde() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testComplexDenyOnRootNode() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testPerf() throws Exception + { + runAs("admin"); + + //TransactionService transactionService = serviceRegistry.getTransactionService(); + //UserTransaction tx = transactionService.getUserTransaction(); + //tx.begin(); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n2 = nodeService.createNode(n1, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}two"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n3 = nodeService.createNode(n2, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}three"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n4 = nodeService.createNode(n3, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}four"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n5 = nodeService.createNode(n4, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}five"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n6 = nodeService.createNode(n5, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}six"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n7 = nodeService.createNode(n6, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}seven"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n8 = nodeService.createNode(n7, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}eight"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n9 = nodeService.createNode(n8, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}nine"), + ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n10 = nodeService.createNode(n9, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}ten"), + ContentModel.TYPE_FOLDER).getChildRef(); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + // permissionService.setPermission(new SimplePermissionEntry(n9, + // getPermission(PermissionService.READ), + // "andy", AccessStatus.ALLOWED)); + // permissionService.setPermission(new SimplePermissionEntry(n10, + // getPermission(PermissionService.READ), + // "andy", AccessStatus.ALLOWED)); + + long start; + long end; + long time = 0; + for (int i = 0; i < 1000; i++) + { + getSession().flush(); + //getSession().clear(); + start = System.nanoTime(); + assertTrue(permissionService.hasPermission(n10, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + end = System.nanoTime(); + time += (end - start); + } + System.out.println("Time is " + (time / 1000000000.0)); + // assertTrue((time / 1000000000.0) < 60.0); + + time = 0; + for (int i = 0; i < 1000; i++) + { + start = System.nanoTime(); + assertTrue(permissionService.hasPermission(n10, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + end = System.nanoTime(); + time += (end - start); + } + System.out.println("Time is " + (time / 1000000000.0)); + // assertTrue((time / 1000000000.0) < 2.0); + + //tx.rollback(); + } + + public void testAllPermissions() + { + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.ALLOWED)); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.DENIED)); + assertEquals(3, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + } + + + public void testOldAllPermissions() + { + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE, "andy", AccessStatus.ALLOWED)); + assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), "andy", AccessStatus.DENIED)); + assertEquals(3, permissionService.getAllSetPermissions(rootNodeRef).size()); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.ALL_PERMISSIONS)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, PermissionServiceImpl.OLD_ALL_PERMISSIONS_REFERENCE) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + } + + + public void testAuthenticatedAuthority() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + ROLE_AUTHENTICATED, AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + ROLE_AUTHENTICATED, AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), ROLE_AUTHENTICATED, AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), ROLE_AUTHENTICATED, AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testAllAuthorities() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + permissionService.getAllAuthorities(), AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + permissionService.getAllAuthorities(), AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), permissionService.getAllAuthorities(), AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ), permissionService.getAllAuthorities(), AccessStatus.ALLOWED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testAllPermissionsAllAuthorities() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), permissionService.getAllAuthorities(), AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + permissionService.getAllAuthorities(), AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, permissionService + .getAllPermissionReference(), permissionService.getAllAuthorities(), AccessStatus.DENIED)); + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testGroupAndUserInteraction() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + ROLE_AUTHENTICATED, AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.DENIED)); + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testInheritPermissions() + { + runAs("admin"); + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n2 = nodeService.createNode(n1, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}two"), + ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ), "andy", + AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setInheritParentPermissions(n2, false); + + runAs("andy"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setInheritParentPermissions(n2, true); + + runAs("andy"); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + } + + public void testAncestorRequirementAndInheritance() + { + runAs("admin"); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n2 = nodeService.createNode(n1, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}two"), + ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ_CHILDREN), + "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(n2, getPermission(PermissionService.READ_PROPERTIES), + "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ_CHILDREN), + "andy", AccessStatus.DENIED)); + permissionService.setInheritParentPermissions(n2, false); + + runAs("andy"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setInheritParentPermissions(n2, true); + + runAs("andy"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + } + + public void testEffectiveComposite() + { + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + } + + public void testContentPermissions() + { + runAs("admin"); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + NodeRef n2 = nodeService.createNode(n1, ContentModel.ASSOC_CONTAINS, QName.createQName("{namespace}two"), + ContentModel.TYPE_CONTENT).getChildRef(); + + runAs("andy"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ_CHILDREN), + "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(n2, getPermission(PermissionService.READ_CHILDREN), + "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(n2, getPermission(PermissionService.READ_PROPERTIES), + "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(n2, getPermission(PermissionService.READ_CONTENT), + "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(n2, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + permissionService.deletePermission(new SimplePermissionEntry(n2, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + permissionService.deletePermission(new SimplePermissionEntry(n2, getPermission(PermissionService.READ_CONTENT), + "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(n2, getPermission(PermissionService.READ), "andy", + AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(n2, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + } + + public void testAllPermissionSet() + { + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.FULL_CONTROL), "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.FULL_CONTROL), "andy", AccessStatus.DENIED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_PROPERTIES), "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + permissionService.deletePermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.FULL_CONTROL), "andy", AccessStatus.DENIED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CONTENT)) == AccessStatus.ALLOWED); + + } + + public void testChildrenRequirements() + { + if (!personService.createMissingPeople()) + { + assertEquals(1, nodeService.getChildAssocs(rootNodeRef).size()); + } + runAs("andy"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.DELETE), + "andy", AccessStatus.ALLOWED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + + runAs("andy"); + assertTrue(permissionService.hasPermission(systemNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(systemNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(systemNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(systemNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(systemNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(systemNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + + permissionService.setPermission(new SimplePermissionEntry(systemNodeRef, + getPermission(PermissionService.DELETE), "andy", AccessStatus.DENIED)); + + runAs("andy"); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + // The following are now true as we have no cascade delete check + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_CHILDREN)) == AccessStatus.ALLOWED); + runAs("lemur"); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_CHILDREN)) == AccessStatus.ALLOWED); + assertFalse(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.DELETE_NODE)) == AccessStatus.ALLOWED); + + } + + public void testClearPermission() + { + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "andy", AccessStatus.ALLOWED)); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), + "lemur", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, + getPermission(PermissionService.READ_CHILDREN), "lemur", AccessStatus.ALLOWED)); + assertEquals(4, permissionService.getAllSetPermissions(rootNodeRef).size()); + + permissionService.clearPermission(rootNodeRef, "andy"); + assertEquals(2, permissionService.getAllSetPermissions(rootNodeRef).size()); + permissionService.clearPermission(rootNodeRef, "lemur"); + assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); + + } + + + // TODO: Test permissions on missing nodes + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/PermissionsDAO.java b/source/java/org/alfresco/repo/security/permissions/impl/PermissionsDAO.java new file mode 100644 index 0000000000..733e1747b7 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/PermissionsDAO.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.repo.security.permissions.NodePermissionEntry; +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * The API for accessing persisted Alfresco permissions. + * + * @author andyh + */ +public interface PermissionsDAO +{ + /** + * Get the permissions that have been set on a given node. + * + * @param nodeRef + * @return + */ + public NodePermissionEntry getPermissions(NodeRef nodeRef); + + /** + * Delete all the permissions on a given node. + * The node permission and all the permission entries it contains will be deleted. + * + * @param nodeRef + */ + public void deletePermissions(NodeRef nodeRef); + + /** + * Delete all the permissions on a given node. + * The node permission and all the permission entries it contains will be deleted. + * + * @param nodePermissionEntry + */ + public void deletePermissions(NodePermissionEntry nodePermissionEntry); + + + /** + * Delete as single permission entry. + * This deleted one permission on the node. It does not affect the persistence of any other permissions. + * + * @param permissionEntry + */ + public void deletePermissions(PermissionEntry permissionEntry); + + /** + * + * Delete as single permission entry, if a match is found. + * This deleted one permission on the node. It does not affect the persistence of any other permissions. + * + * @param nodeRef + * @param authority + * @param perm + * @param allow + */ + public void deletePermissions(NodeRef nodeRef, String authority, PermissionReference perm, boolean allow); + + /** + * Set a permission on a node. + * If the node has no permissions set then a default node permission (allowing inheritance) will be created to + * contain the permission entry. + * + * @param nodeRef + * @param authority + * @param perm + * @param allow + */ + public void setPermission(NodeRef nodeRef, String authority, PermissionReference perm, boolean allow); + + /** + * Create a persisted permission entry given and other representation of a permission entry. + * + * @param permissionEntry + */ + public void setPermission(PermissionEntry permissionEntry); + + /** + * Create a persisted node permission entry given a template object from which to copy. + * + * @param nodePermissionEntry + */ + public void setPermission(NodePermissionEntry nodePermissionEntry); + + /** + * Set the inheritance behaviour for permissions on a given node. + * + * @param nodeRef + * @param inheritParentPermissions + */ + public void setInheritParentPermissions(NodeRef nodeRef, boolean inheritParentPermissions); + + /** + * Return the inheritance behaviour for permissions on a given node. + * + * @param nodeRef + * @return inheritParentPermissions + */ + public boolean getInheritParentPermissions(NodeRef nodeRef); + + /** + * Clear all the permissions set for a given authentication + * + * @param nodeRef + * @param authority + */ + public void clearPermission(NodeRef nodeRef, String authority); + + /** + * Remove all permissions for the specvified authority + * @param authority + */ + public void deleteAllPermissionsForAuthority(String authority); + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/RequiredPermission.java b/source/java/org/alfresco/repo/security/permissions/impl/RequiredPermission.java new file mode 100644 index 0000000000..7779f27140 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/RequiredPermission.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.service.namespace.QName; + +/** + * Store and read the definition of a required permission. + * + * @author andyh + */ +public class RequiredPermission extends PermissionReferenceImpl +{ + public enum On { + PARENT, NODE, CHILDREN + }; + + private On on; + + boolean implies; + + public RequiredPermission(QName qName, String name, On on, boolean implies) + { + super(qName, name); + this.on = on; + this.implies = implies; + } + + public boolean isImplies() + { + return implies; + } + + public On getOn() + { + return on; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/SimpleNodePermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/SimpleNodePermissionEntry.java new file mode 100644 index 0000000000..b06b2b8274 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/SimpleNodePermissionEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import java.io.Serializable; +import java.util.Set; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * A simple object representation of a node permission entry + * + * @author andyh + */ +public class SimpleNodePermissionEntry extends AbstractNodePermissionEntry implements Serializable +{ + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 8157870444595023347L; + + /* + * The node + */ + private NodeRef nodeRef; + + /* + * Are permissions inherited? + */ + private boolean inheritPermissions; + + /* + * The set of permission entries. + */ + private Set permissionEntries; + + + public SimpleNodePermissionEntry(NodeRef nodeRef, boolean inheritPermissions, Set permissionEntries) + { + super(); + this.nodeRef = nodeRef; + this.inheritPermissions = inheritPermissions; + this.permissionEntries = permissionEntries; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public boolean inheritPermissions() + { + return inheritPermissions; + } + + public Set getPermissionEntries() + { + return permissionEntries; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionEntry.java new file mode 100644 index 0000000000..6534d7d210 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionEntry.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * A simple object representation of a permission entry. + * + * @author andyh + */ +public class SimplePermissionEntry extends AbstractPermissionEntry +{ + + /* + * The node ref to which the permissoin applies + */ + private NodeRef nodeRef; + + /* + * The permission reference - as a simple permission reference + */ + private PermissionReference permissionReference; + + /* + * The authority to which the permission aplies + */ + private String authority; + + /* + * The access mode for the permission + */ + private AccessStatus accessStatus; + + + + public SimplePermissionEntry(NodeRef nodeRef, PermissionReference permissionReference, String authority, AccessStatus accessStatus) + { + super(); + this.nodeRef = nodeRef; + this.permissionReference = permissionReference; + this.authority = authority; + this.accessStatus = accessStatus; + } + + public PermissionReference getPermissionReference() + { + return permissionReference; + } + + public String getAuthority() + { + return authority; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public boolean isDenied() + { + return accessStatus == AccessStatus.DENIED; + } + + public boolean isAllowed() + { + return accessStatus == AccessStatus.ALLOWED; + } + + public AccessStatus getAccessStatus() + { + return accessStatus; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionReference.java b/source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionReference.java new file mode 100644 index 0000000000..4ee0855159 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/SimplePermissionReference.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl; + +import org.alfresco.service.namespace.QName; + +/** + * A simple permission reference. + * + * @author andyh + */ +public class SimplePermissionReference extends AbstractPermissionReference +{ + /* + * The type + */ + private QName qName; + + /* + * The name of the permission + */ + private String name; + + + public SimplePermissionReference(QName qName, String name) + { + super(); + this.qName = qName; + this.name = name; + } + + public QName getQName() + { + return qName; + } + + public String getName() + { + return name; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java new file mode 100644 index 0000000000..19ecc1cd31 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +import net.sf.acegisecurity.AccessDeniedException; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.afterinvocation.AfterInvocationProvider; + +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, InitializingBean +{ + private static Log log = LogFactory.getLog(ACLEntryAfterInvocationProvider.class); + + private static final String AFTER_ACL_NODE = "AFTER_ACL_NODE"; + + private static final String AFTER_ACL_PARENT = "AFTER_ACL_PARENT"; + + private PermissionService permissionService; + + private NamespacePrefixResolver nspr; + + private NodeService nodeService; + + private AuthenticationService authenticationService; + + public ACLEntryAfterInvocationProvider() + { + super(); + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public PermissionService getPermissionService() + { + return permissionService; + } + + public NamespacePrefixResolver getNamespacePrefixResolver() + { + return nspr; + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver nspr) + { + this.nspr = nspr; + } + + public NodeService getNodeService() + { + return nodeService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public AuthenticationService getAuthenticationService() + { + return authenticationService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void afterPropertiesSet() throws Exception + { + if (permissionService == null) + { + throw new IllegalArgumentException("There must be a permission service"); + } + if (nspr == null) + { + throw new IllegalArgumentException("There must be a namespace service"); + } + if (nodeService == null) + { + throw new IllegalArgumentException("There must be a node service"); + } + if (authenticationService == null) + { + throw new IllegalArgumentException("There must be an authentication service"); + } + + } + + public Object decide(Authentication authentication, Object object, ConfigAttributeDefinition config, + Object returnedObject) throws AccessDeniedException + { + if (log.isDebugEnabled()) + { + MethodInvocation mi = (MethodInvocation) object; + log.debug("Method: " + mi.getMethod().toString()); + } + try + { + if (authenticationService.isCurrentUserTheSystemUser()) + { + if (log.isDebugEnabled()) + { + log.debug("Allowing system user access"); + } + return returnedObject; + } + else if (returnedObject == null) + { + if (log.isDebugEnabled()) + { + log.debug("Allowing null object access"); + } + return null; + } + else if (StoreRef.class.isAssignableFrom(returnedObject.getClass())) + { + if (log.isDebugEnabled()) + { + log.debug("Store access"); + } + return decide(authentication, object, config, nodeService.getRootNode((StoreRef) returnedObject)) + .getStoreRef(); + } + else if (NodeRef.class.isAssignableFrom(returnedObject.getClass())) + { + if (log.isDebugEnabled()) + { + log.debug("Node access"); + } + return decide(authentication, object, config, (NodeRef) returnedObject); + } + else if (ChildAssociationRef.class.isAssignableFrom(returnedObject.getClass())) + { + if (log.isDebugEnabled()) + { + log.debug("Child Association access"); + } + return decide(authentication, object, config, (ChildAssociationRef) returnedObject); + } + else if (ResultSet.class.isAssignableFrom(returnedObject.getClass())) + { + if (log.isDebugEnabled()) + { + log.debug("Result Set access"); + } + return decide(authentication, object, config, (ResultSet) returnedObject); + } + else if (Collection.class.isAssignableFrom(returnedObject.getClass())) + { + if (log.isDebugEnabled()) + { + log.debug("Collection Access"); + } + return decide(authentication, object, config, (Collection) returnedObject); + } + else if (returnedObject.getClass().isArray()) + { + if (log.isDebugEnabled()) + { + log.debug("Array Access"); + } + return decide(authentication, object, config, (Object[]) returnedObject); + } + else + { + if (log.isDebugEnabled()) + { + log.debug("Uncontrolled object - access allowed for " + object.getClass().getName()); + } + return returnedObject; + } + } + catch (AccessDeniedException ade) + { + if (log.isDebugEnabled()) + { + log.debug("Access denied"); + ade.printStackTrace(); + } + throw ade; + } + catch (RuntimeException re) + { + if (log.isDebugEnabled()) + { + log.debug("Access denied by runtime exception"); + re.printStackTrace(); + } + throw re; + } + + } + + public NodeRef decide(Authentication authentication, Object object, ConfigAttributeDefinition config, + NodeRef returnedObject) throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = null; + + if (cad.typeString.equals(AFTER_ACL_NODE)) + { + testNodeRef = returnedObject; + } + else if (cad.typeString.equals(AFTER_ACL_PARENT)) + { + testNodeRef = nodeService.getPrimaryParent(returnedObject).getParentRef(); + } + + if ((testNodeRef != null) + && (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED)) + { + throw new AccessDeniedException("Access Denied"); + } + + } + + return returnedObject; + } + + private List extractSupportedDefinitions(ConfigAttributeDefinition config) + { + List definitions = new ArrayList(); + Iterator iter = config.getConfigAttributes(); + + while (iter.hasNext()) + { + ConfigAttribute attr = (ConfigAttribute) iter.next(); + + if (this.supports(attr)) + { + definitions.add(new ConfigAttributeDefintion(attr)); + } + + } + return definitions; + } + + public ChildAssociationRef decide(Authentication authentication, Object object, ConfigAttributeDefinition config, + ChildAssociationRef returnedObject) throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = null; + + if (cad.typeString.equals(AFTER_ACL_NODE)) + { + testNodeRef = ((ChildAssociationRef) returnedObject).getChildRef(); + } + else if (cad.typeString.equals(AFTER_ACL_PARENT)) + { + testNodeRef = ((ChildAssociationRef) returnedObject).getParentRef(); + } + + if ((testNodeRef != null) + && (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED)) + { + throw new AccessDeniedException("Access Denied"); + } + + } + + return returnedObject; + } + + public ResultSet decide(Authentication authentication, Object object, ConfigAttributeDefinition config, + ResultSet returnedObject) throws AccessDeniedException + + { + FilteringResultSet filteringResultSet = new FilteringResultSet((ResultSet) returnedObject); + + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + for (int i = 0; i < returnedObject.length(); i++) + { + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + filteringResultSet.setIncluded(i, true); + NodeRef testNodeRef = null; + if (cad.typeString.equals(AFTER_ACL_NODE)) + { + testNodeRef = returnedObject.getNodeRef(i); + } + else if (cad.typeString.equals(AFTER_ACL_PARENT)) + { + testNodeRef = returnedObject.getChildAssocRef(i).getParentRef(); + } + + if (filteringResultSet.getIncluded(i) + && (testNodeRef != null) + && (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED)) + { + filteringResultSet.setIncluded(i, false); + } + } + } + + return filteringResultSet; + } + + public Collection decide(Authentication authentication, Object object, ConfigAttributeDefinition config, + Collection returnedObject) throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + Set removed = new HashSet(); + + if (log.isDebugEnabled()) + { + log.debug("Entries are " + supportedDefinitions); + } + + for (Object nextObject : returnedObject) + { + boolean allowed = true; + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = null; + + if (cad.typeString.equals(AFTER_ACL_NODE)) + { + if (StoreRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = nodeService.getRootNode((StoreRef) nextObject); + if (log.isDebugEnabled()) + { + log.debug("\tNode Test on store " + nodeService.getPath(testNodeRef)); + } + } + else if (NodeRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = (NodeRef) nextObject; + if (log.isDebugEnabled()) + { + log.debug("\tNode Test on node " + nodeService.getPath(testNodeRef)); + } + } + else if (ChildAssociationRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((ChildAssociationRef) nextObject).getChildRef(); + if (log.isDebugEnabled()) + { + log.debug("\tNode Test on child association ref using " + nodeService.getPath(testNodeRef)); + } + } + else + { + throw new ACLEntryVoterException( + "The specified parameter is not a collection of NodeRefs or ChildAssociationRefs"); + } + } + else if (cad.typeString.equals(AFTER_ACL_PARENT)) + { + if (StoreRef.class.isAssignableFrom(nextObject.getClass())) + { + // Will be allowed + testNodeRef = null; + if (log.isDebugEnabled()) + { + log.debug("\tParent Test on store "); + } + } + else if (NodeRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = nodeService.getPrimaryParent((NodeRef) nextObject).getParentRef(); + if (log.isDebugEnabled()) + { + log.debug("\tParent test on node " + nodeService.getPath(testNodeRef)); + } + } + else if (ChildAssociationRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((ChildAssociationRef) nextObject).getParentRef(); + if (log.isDebugEnabled()) + { + log.debug("\tParent Test on child association ref using " + + nodeService.getPath(testNodeRef)); + } + } + else + { + throw new ACLEntryVoterException( + "The specified parameter is not a collection of NodeRefs or ChildAssociationRefs"); + } + } + + if (allowed + && (testNodeRef != null) + && (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED)) + { + allowed = false; + } + } + if (!allowed) + { + removed.add(nextObject); + } + } + for (Object toRemove : removed) + { + while (returnedObject.remove(toRemove)) + ; + } + return returnedObject; + } + + public Object[] decide(Authentication authentication, Object object, ConfigAttributeDefinition config, + Object[] returnedObject) throws AccessDeniedException + + { + BitSet incudedSet = new BitSet(returnedObject.length); + + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + for (int i = 0, l = returnedObject.length; i < l; i++) + { + Object current = returnedObject[i]; + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + incudedSet.set(i, true); + NodeRef testNodeRef = null; + if (cad.typeString.equals(AFTER_ACL_NODE)) + { + if (StoreRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = nodeService.getRootNode((StoreRef) current); + } + else if (NodeRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = (NodeRef) current; + } + else if (ChildAssociationRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((ChildAssociationRef) current).getChildRef(); + } + else + { + throw new ACLEntryVoterException("The specified array is not of NodeRef or ChildAssociationRef"); + } + } + + else if (cad.typeString.equals(AFTER_ACL_PARENT)) + { + if (StoreRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = null; + } + else if (NodeRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = nodeService.getPrimaryParent((NodeRef) current).getParentRef(); + } + else if (ChildAssociationRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((ChildAssociationRef) current).getParentRef(); + } + else + { + throw new ACLEntryVoterException("The specified array is not of NodeRef or ChildAssociationRef"); + } + } + + if (incudedSet.get(i) + && (testNodeRef != null) + && (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED)) + { + incudedSet.set(i, false); + } + + } + } + + if (incudedSet.cardinality() == returnedObject.length) + { + return returnedObject; + } + else + { + Object[] answer = new Object[incudedSet.cardinality()]; + for (int i = incudedSet.nextSetBit(0), p = 0; i >= 0; i = incudedSet.nextSetBit(++i), p++) + { + answer[p] = returnedObject[i]; + } + return answer; + } + } + + public boolean supports(ConfigAttribute attribute) + { + if ((attribute.getAttribute() != null) + && (attribute.getAttribute().startsWith(AFTER_ACL_NODE) || attribute.getAttribute().startsWith( + AFTER_ACL_PARENT))) + { + return true; + } + else + { + return false; + } + } + + public boolean supports(Class clazz) + { + return (MethodInvocation.class.isAssignableFrom(clazz)); + } + + private class ConfigAttributeDefintion + { + + String typeString; + + SimplePermissionReference required; + + ConfigAttributeDefintion(ConfigAttribute attr) + { + + StringTokenizer st = new StringTokenizer(attr.getAttribute(), ".", false); + if (st.countTokens() != 3) + { + throw new ACLEntryVoterException("There must be three . separated tokens in each config attribute"); + } + typeString = st.nextToken(); + String qNameString = st.nextToken(); + String permissionString = st.nextToken(); + + if (!(typeString.equals(AFTER_ACL_NODE) || typeString.equals(AFTER_ACL_PARENT))) + { + throw new ACLEntryVoterException("Invalid type: must be ACL_NODE or ACL_PARENT"); + } + + QName qName = QName.createQName(qNameString, nspr); + + required = new SimplePermissionReference(qName, permissionString); + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java new file mode 100644 index 0000000000..c04c9b3107 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java @@ -0,0 +1,884 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.results.ChildAssocRefResultSet; +import org.alfresco.repo.security.permissions.impl.AbstractPermissionTest; +import org.alfresco.repo.security.permissions.impl.SimplePermissionEntry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.aop.target.SingletonTargetSource; + +public class ACLEntryAfterInvocationTest extends AbstractPermissionTest +{ + + public ACLEntryAfterInvocationTest() + { + super(); + } + + public void testBasicAllowNullNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { null }); + assertNull(answer); + } + + public void testBasicAllowNullStore() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoStoreRef", new Class[] { StoreRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { null }); + assertNull(answer); + } + + public void testBasicAllowUnrecognisedObject() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoObject", new Class[] { Object.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { "noodle" }); + assertNotNull(answer); + } + + public void testBasicDenyStore() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoStoreRef", new Class[] { StoreRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + try + { + Object answer = method.invoke(proxy, new Object[] { rootNodeRef.getStoreRef() }); + assertNotNull(answer); + } + catch (InvocationTargetException e) + { + + } + + } + + public void testBasicDenyNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + try + { + Object answer = method.invoke(proxy, new Object[] { rootNodeRef }); + assertNotNull(answer); + } + catch (InvocationTargetException e) + { + + } + + } + + public void testBasicAllowNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object answer = method.invoke(proxy, new Object[] { rootNodeRef }); + assertEquals(answer, rootNodeRef); + + } + + public void testBasicAllowStore() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoStoreRef", new Class[] { StoreRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object answer = method.invoke(proxy, new Object[] { rootNodeRef.getStoreRef() }); + assertEquals(answer, rootNodeRef.getStoreRef()); + + } + + public void testBasicAllowNodeParent() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_PARENT.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { rootNodeRef }); + assertEquals(answer, rootNodeRef); + + try + { + answer = method.invoke(proxy, new Object[] { systemNodeRef }); + assertNotNull(answer); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + answer = method.invoke(proxy, new Object[] { systemNodeRef }); + assertEquals(answer, systemNodeRef); + } + + public void testBasicAllowNullChildAssociationRef1() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoChildAssocRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { null }); + assertNull(answer); + } + + public void testBasicAllowNullChildAssociationRef2() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoChildAssocRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_PARENT.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { null }); + assertNull(answer); + } + + public void testBasicDenyChildAssocRef1() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoChildAssocRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + try + { + Object answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + assertNotNull(answer); + } + catch (InvocationTargetException e) + { + + } + + try + { + Object answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + assertNotNull(answer); + } + catch (InvocationTargetException e) + { + + } + + } + + public void testBasicDenyChildAssocRef2() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoChildAssocRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_PARENT.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + Object answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + assertNotNull(answer); + + try + { + answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + assertNotNull(answer); + } + catch (InvocationTargetException e) + { + + } + + } + + public void testBasicAllowChildAssociationRef1() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoChildAssocRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + assertEquals(answer, nodeService.getPrimaryParent(rootNodeRef)); + + answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + assertEquals(answer, nodeService.getPrimaryParent(systemNodeRef)); + + } + + public void testBasicAllowChildAssociationRef2() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("echoChildAssocRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_PARENT.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + assertEquals(answer, nodeService.getPrimaryParent(rootNodeRef)); + + answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + assertEquals(answer, nodeService.getPrimaryParent(systemNodeRef)); + } + + public void testBasicAllowNullResultSet() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method methodResultSet = o.getClass().getMethod("echoResultSet", new Class[] { ResultSet.class }); + Method methodCollection = o.getClass().getMethod("echoCollection", new Class[] { Collection.class }); + Method methodArray = o.getClass().getMethod("echoArray", new Class[] { Object[].class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + List nodeRefList = new ArrayList(); + NodeRef[] nodeRefArray = new NodeRef[0]; + + Set nodeRefSet = new HashSet(); + + List carList = new ArrayList(); + + ChildAssociationRef[] carArray = new ChildAssociationRef[0]; + + Set carSet = new HashSet(); + + ChildAssocRefResultSet rsIn = new ChildAssocRefResultSet(nodeService, nodeRefList, null, false); + + assertEquals(0, rsIn.length()); + ResultSet answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(0, answerResultSet.length()); + Collection answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(0, answerCollection.size()); + Object[] answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(0, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(0, answerArray.length); + + assertEquals(0, rsIn.length()); + answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { null }); + assertNull(answerResultSet); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { null }); + assertNull(answerCollection); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { null }); + assertNull(answerArray); + } + + public void testResultSetFilterAll() throws Exception + { + runAs("admin"); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + + Object o = new ClassWithMethods(); + Method methodResultSet = o.getClass().getMethod("echoResultSet", new Class[] { ResultSet.class }); + Method methodCollection = o.getClass().getMethod("echoCollection", new Class[] { Collection.class }); + Method methodArray = o.getClass().getMethod("echoArray", new Class[] { Object[].class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + List nodeRefList = new ArrayList(); + nodeRefList.add(rootNodeRef); + nodeRefList.add(systemNodeRef); + nodeRefList.add(n1); + nodeRefList.add(n1); + + NodeRef[] nodeRefArray = nodeRefList.toArray(new NodeRef[] {}); + + Set nodeRefSet = new HashSet(); + nodeRefSet.addAll(nodeRefList); + + List carList = new ArrayList(); + carList.add(nodeService.getPrimaryParent(rootNodeRef)); + carList.add(nodeService.getPrimaryParent(systemNodeRef)); + carList.add(nodeService.getPrimaryParent(n1)); + carList.add(nodeService.getPrimaryParent(n1)); + + ChildAssociationRef[] carArray = carList.toArray(new ChildAssociationRef[] {}); + + Set carSet = new HashSet(); + carSet.addAll(carList); + + ChildAssocRefResultSet rsIn = new ChildAssocRefResultSet(nodeService, nodeRefList, null, false); + + assertEquals(4, rsIn.length()); + ResultSet answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(0, answerResultSet.length()); + Collection answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(0, answerCollection.size()); + Object[] answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(0, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(0, answerArray.length); + } + + public void testResultSetFilterForNullParentOnly() throws Exception + { + runAs("admin"); + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + + Object o = new ClassWithMethods(); + Method methodResultSet = o.getClass().getMethod("echoResultSet", new Class[] { ResultSet.class }); + Method methodCollection = o.getClass().getMethod("echoCollection", new Class[] { Collection.class }); + Method methodArray = o.getClass().getMethod("echoArray", new Class[] { Object[].class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_PARENT.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + List nodeRefList = new ArrayList(); + nodeRefList.add(rootNodeRef); + nodeRefList.add(systemNodeRef); + nodeRefList.add(n1); + nodeRefList.add(n1); + + NodeRef[] nodeRefArray = nodeRefList.toArray(new NodeRef[] {}); + + Set nodeRefSet = new HashSet(); + nodeRefSet.addAll(nodeRefList); + + List carList = new ArrayList(); + carList.add(nodeService.getPrimaryParent(rootNodeRef)); + carList.add(nodeService.getPrimaryParent(systemNodeRef)); + carList.add(nodeService.getPrimaryParent(n1)); + carList.add(nodeService.getPrimaryParent(n1)); + + ChildAssociationRef[] carArray = carList.toArray(new ChildAssociationRef[] {}); + + Set carSet = new HashSet(); + carSet.addAll(carList); + + ChildAssocRefResultSet rsIn = new ChildAssocRefResultSet(nodeService, nodeRefList, null, false); + + + assertEquals(4, rsIn.length()); + ResultSet answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(1, answerResultSet.length()); + Collection answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(1, answerCollection.size()); + Object[] answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(1, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(1, answerArray.length); + } + + public void testResultSetFilterNone1() throws Exception + { + runAs("admin"); + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + + Object o = new ClassWithMethods(); + Method methodResultSet = o.getClass().getMethod("echoResultSet", new Class[] { ResultSet.class }); + Method methodCollection = o.getClass().getMethod("echoCollection", new Class[] { Collection.class }); + Method methodArray = o.getClass().getMethod("echoArray", new Class[] { Object[].class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_NODE.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + List nodeRefList = new ArrayList(); + nodeRefList.add(rootNodeRef); + nodeRefList.add(systemNodeRef); + nodeRefList.add(n1); + nodeRefList.add(n1); + + List mixedRefList = new ArrayList(); + mixedRefList.add(rootNodeRef); + mixedRefList.add(systemNodeRef); + mixedRefList.add(n1); + mixedRefList.add(n1); + mixedRefList.add(rootNodeRef.getStoreRef()); + + NodeRef[] nodeRefArray = nodeRefList.toArray(new NodeRef[] {}); + + + Set nodeRefSet = new HashSet(); + nodeRefSet.addAll(nodeRefList); + + Set mixedRefSet = new HashSet(); + mixedRefSet.addAll(mixedRefList); + + List carList = new ArrayList(); + carList.add(nodeService.getPrimaryParent(rootNodeRef)); + carList.add(nodeService.getPrimaryParent(systemNodeRef)); + carList.add(nodeService.getPrimaryParent(n1)); + carList.add(nodeService.getPrimaryParent(n1)); + + ChildAssociationRef[] carArray = carList.toArray(new ChildAssociationRef[] {}); + + Set carSet = new HashSet(); + carSet.addAll(carList); + + ChildAssocRefResultSet rsIn = new ChildAssocRefResultSet(nodeService, nodeRefList, null, false); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + assertEquals(4, rsIn.length()); + ResultSet answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(4, answerResultSet.length()); + Collection answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefList }); + assertEquals(5, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(3, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefSet }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(3, answerCollection.size()); + Object[] answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(4, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(4, answerArray.length); + + permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + + assertEquals(4, rsIn.length()); + answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(2, answerResultSet.length()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(2, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefList }); + assertEquals(3, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(2, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefSet }); + assertEquals(3, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(2, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(2, answerCollection.size()); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(2, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(2, answerArray.length); + + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + + assertEquals(4, rsIn.length()); + answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(0, answerResultSet.length()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefSet }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(0, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(0, answerCollection.size()); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(0, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(0, answerArray.length); + + } + + public void testResultSetFilterNone2() throws Exception + { + runAs("admin"); + + NodeRef n1 = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("{namespace}one"), ContentModel.TYPE_FOLDER).getChildRef(); + + runAs("andy"); + + Object o = new ClassWithMethods(); + Method methodResultSet = o.getClass().getMethod("echoResultSet", new Class[] { ResultSet.class }); + Method methodCollection = o.getClass().getMethod("echoCollection", new Class[] { Collection.class }); + Method methodArray = o.getClass().getMethod("echoArray", new Class[] { Object[].class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("AFTER_ACL_PARENT.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + List nodeRefList = new ArrayList(); + nodeRefList.add(rootNodeRef); + nodeRefList.add(systemNodeRef); + nodeRefList.add(n1); + nodeRefList.add(n1); + + List mixedRefList = new ArrayList(); + mixedRefList.add(rootNodeRef); + mixedRefList.add(systemNodeRef); + mixedRefList.add(n1); + mixedRefList.add(n1); + mixedRefList.add(rootNodeRef.getStoreRef()); + + NodeRef[] nodeRefArray = nodeRefList.toArray(new NodeRef[] {}); + + Set nodeRefSet = new HashSet(); + nodeRefSet.addAll(nodeRefList); + + Set mixedRefSet = new HashSet(); + mixedRefSet.addAll(mixedRefList); + + List carList = new ArrayList(); + carList.add(nodeService.getPrimaryParent(rootNodeRef)); + carList.add(nodeService.getPrimaryParent(systemNodeRef)); + carList.add(nodeService.getPrimaryParent(n1)); + carList.add(nodeService.getPrimaryParent(n1)); + + ChildAssociationRef[] carArray = carList.toArray(new ChildAssociationRef[] {}); + + Set carSet = new HashSet(); + carSet.addAll(carList); + + ChildAssocRefResultSet rsIn = new ChildAssocRefResultSet(nodeService, nodeRefList, null, false); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + assertEquals(4, rsIn.length()); + ResultSet answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(4, answerResultSet.length()); + Collection answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefList }); + assertEquals(5, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(3, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefSet }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(3, answerCollection.size()); + Object[] answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(4, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(4, answerArray.length); + + permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + + assertEquals(4, rsIn.length()); + answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(4, answerResultSet.length()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefList }); + assertEquals(5, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(3, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefSet }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(4, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(3, answerCollection.size()); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(4, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(4, answerArray.length); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); + + assertEquals(4, rsIn.length()); + answerResultSet = (ResultSet) methodResultSet.invoke(proxy, new Object[] { rsIn }); + assertEquals(1, answerResultSet.length()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefList }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefList }); + assertEquals(2, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { nodeRefSet }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { mixedRefSet }); + assertEquals(2, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carList }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke(proxy, new Object[] { carSet }); + assertEquals(1, answerCollection.size()); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { nodeRefArray }); + assertEquals(1, answerArray.length); + answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); + assertEquals(1, answerArray.length); + + } + + public static class ClassWithMethods + { + + public Object echoObject(Object o) + { + return o; + } + + public StoreRef echoStoreRef(StoreRef storeRef) + { + return storeRef; + } + + public NodeRef echoNodeRef(NodeRef nodeRef) + { + return nodeRef; + } + + public ChildAssociationRef echoChildAssocRef(ChildAssociationRef car) + { + return car; + } + + public ResultSet echoResultSet(ResultSet rs) + { + return rs; + } + + public Collection echoCollection(Collection nrc) + { + return nrc; + } + + public T[] echoArray(T[] nra) + { + return nra; + } + + } + + public class Interceptor implements MethodInterceptor + { + ConfigAttributeDefinition cad = new ConfigAttributeDefinition(); + + Interceptor(final String config) + { + cad.addConfigAttribute(new ConfigAttribute() + { + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 1L; + + public String getAttribute() + { + return config; + } + + }); + } + + public Object invoke(MethodInvocation invocation) throws Throwable + { + ACLEntryAfterInvocationProvider after = new ACLEntryAfterInvocationProvider(); + after.setNamespacePrefixResolver(namespacePrefixResolver); + after.setPermissionService(permissionService); + after.setNodeService(nodeService); + after.setAuthenticationService(authenticationService); + + Object returnObject = invocation.proceed(); + return after.decide(null, invocation, cad, returnObject); + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java new file mode 100644 index 0000000000..1e01b80e5e --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author andyh + */ + +public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean +{ + private static Log log = LogFactory.getLog(ACLEntryVoter.class); + + private static final String ACL_NODE = "ACL_NODE"; + + private static final String ACL_PARENT = "ACL_PARENT"; + + private static final String ACL_ALLOW = "ACL_ALLOW"; + + private static final String ACL_METHOD = "ACL_METHOD"; + + private PermissionService permissionService; + + private NamespacePrefixResolver nspr; + + private NodeService nodeService; + + private AuthenticationService authenticationService; + + private AuthorityService authorityService; + + public ACLEntryVoter() + { + super(); + } + + // ~ Methods + // ================================================================ + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public PermissionService getPermissionService() + { + return permissionService; + } + + public NamespacePrefixResolver getNamespacePrefixResolver() + { + return nspr; + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver nspr) + { + this.nspr = nspr; + } + + public NodeService getNodeService() + { + return nodeService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public AuthenticationService getAuthenticationService() + { + return authenticationService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void afterPropertiesSet() throws Exception + { + if (permissionService == null) + { + throw new IllegalArgumentException("There must be a permission service"); + } + if (nspr == null) + { + throw new IllegalArgumentException("There must be a namespace service"); + } + if (nodeService == null) + { + throw new IllegalArgumentException("There must be a node service"); + } + if (authenticationService == null) + { + throw new IllegalArgumentException("There must be an authentication service"); + } + if (authorityService == null) + { + throw new IllegalArgumentException("There must be an authority service"); + } + + } + + public boolean supports(ConfigAttribute attribute) + { + if ((attribute.getAttribute() != null) + && (attribute.getAttribute().startsWith(ACL_NODE) + || attribute.getAttribute().startsWith(ACL_PARENT) + || attribute.getAttribute().startsWith(ACL_ALLOW) || attribute.getAttribute().startsWith( + ACL_METHOD))) + { + return true; + } + else + { + return false; + } + } + + /** + * This implementation supports only MethodSecurityInterceptor, + * because it queries the presented MethodInvocation. + * + * @param clazz + * the secure object + * + * @return true if the secure object is + * MethodInvocation, false otherwise + */ + public boolean supports(Class clazz) + { + return (MethodInvocation.class.isAssignableFrom(clazz)); + } + + public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) + { + if (log.isDebugEnabled()) + { + MethodInvocation mi = (MethodInvocation) object; + log.debug("Method: " + mi.getMethod().toString()); + } + if (authenticationService.isCurrentUserTheSystemUser()) + { + if (log.isDebugEnabled()) + { + log.debug("Access granted for the system user"); + } + return AccessDecisionVoter.ACCESS_GRANTED; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + MethodInvocation invocation = (MethodInvocation) object; + + Method method = invocation.getMethod(); + Class[] params = method.getParameterTypes(); + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = null; + + if (cad.typeString.equals(ACL_ALLOW)) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + else if (cad.typeString.equals(ACL_METHOD)) + { + if (authenticationService.getCurrentUserName().equals(cad.authority)) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + else + { + return authorityService.getAuthorities().contains(cad.authority) ? AccessDecisionVoter.ACCESS_GRANTED + : AccessDecisionVoter.ACCESS_DENIED; + } + } + else if (cad.parameter >= invocation.getArguments().length) + { + continue; + } + else if (cad.typeString.equals(ACL_NODE)) + { + if (StoreRef.class.isAssignableFrom(params[cad.parameter])) + { + if (invocation.getArguments()[cad.parameter] != null) + { + if (log.isDebugEnabled()) + { + log.debug("\tPermission test against the store - using permissions on the root node"); + } + StoreRef storeRef = (StoreRef) invocation.getArguments()[cad.parameter]; + if (nodeService.exists(storeRef)) + { + testNodeRef = nodeService.getRootNode(storeRef); + } + } + } + else if (NodeRef.class.isAssignableFrom(params[cad.parameter])) + { + testNodeRef = (NodeRef) invocation.getArguments()[cad.parameter]; + if (log.isDebugEnabled()) + { + log.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + } + else if (ChildAssociationRef.class.isAssignableFrom(params[cad.parameter])) + { + if (invocation.getArguments()[cad.parameter] != null) + { + testNodeRef = ((ChildAssociationRef) invocation.getArguments()[cad.parameter]).getChildRef(); + if (log.isDebugEnabled()) + { + log.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + } + } + else + { + throw new ACLEntryVoterException("The specified parameter is not a NodeRef or ChildAssociationRef"); + } + } + else if (cad.typeString.equals(ACL_PARENT)) + { + // There is no point having parent permissions for store + // refs + if (NodeRef.class.isAssignableFrom(params[cad.parameter])) + { + NodeRef child = (NodeRef) invocation.getArguments()[cad.parameter]; + if (child != null) + { + testNodeRef = nodeService.getPrimaryParent(child).getParentRef(); + if (log.isDebugEnabled()) + { + log.debug("\tPermission test for parent on node " + nodeService.getPath(testNodeRef)); + } + } + } + else if (ChildAssociationRef.class.isAssignableFrom(params[cad.parameter])) + { + if (invocation.getArguments()[cad.parameter] != null) + { + testNodeRef = ((ChildAssociationRef) invocation.getArguments()[cad.parameter]).getParentRef(); + if (log.isDebugEnabled()) + { + log.debug("\tPermission test for parent on child assoc ref for node " + + nodeService.getPath(testNodeRef)); + } + } + + } + else + { + throw new ACLEntryVoterException("The specified parameter is not a ChildAssociationRef"); + } + } + + if (testNodeRef != null) + { + if (log.isDebugEnabled()) + { + log.debug("\t\tNode ref is not null"); + } + if (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED) + { + if (log.isDebugEnabled()) + { + log.debug("\t\tPermission is denied"); + Thread.dumpStack(); + } + return AccessDecisionVoter.ACCESS_DENIED; + } + } + } + + return AccessDecisionVoter.ACCESS_GRANTED; + } + + private List extractSupportedDefinitions(ConfigAttributeDefinition config) + { + List definitions = new ArrayList(); + Iterator iter = config.getConfigAttributes(); + + while (iter.hasNext()) + { + ConfigAttribute attr = (ConfigAttribute) iter.next(); + + if (this.supports(attr)) + { + definitions.add(new ConfigAttributeDefintion(attr)); + } + + } + return definitions; + } + + private class ConfigAttributeDefintion + { + String typeString; + + SimplePermissionReference required; + + int parameter; + + String authority; + + ConfigAttributeDefintion(ConfigAttribute attr) + { + StringTokenizer st = new StringTokenizer(attr.getAttribute(), ".", false); + if (st.countTokens() < 1) + { + throw new ACLEntryVoterException("There must be at least one token in a config attribute"); + } + typeString = st.nextToken(); + + if (!(typeString.equals(ACL_NODE) || typeString.equals(ACL_PARENT) || typeString.equals(ACL_ALLOW) || typeString + .equals(ACL_METHOD))) + { + throw new ACLEntryVoterException("Invalid type: must be ACL_NODE, ACL_PARENT or ACL_ALLOW"); + } + + if (typeString.equals(ACL_NODE) || typeString.equals(ACL_PARENT)) + { + if (st.countTokens() != 3) + { + throw new ACLEntryVoterException("There must be four . separated tokens in each config attribute"); + } + String numberString = st.nextToken(); + String qNameString = st.nextToken(); + String permissionString = st.nextToken(); + + parameter = Integer.parseInt(numberString); + + QName qName = QName.createQName(qNameString, nspr); + + required = new SimplePermissionReference(qName, permissionString); + } + else if (typeString.equals(ACL_METHOD)) + { + if (st.countTokens() != 1) + { + throw new ACLEntryVoterException( + "There must be two . separated tokens in each group or role config attribute"); + } + authority = st.nextToken(); + } + + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterException.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterException.java new file mode 100644 index 0000000000..211776fa2d --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class ACLEntryVoterException extends AlfrescoRuntimeException +{ + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = -674195849623480512L; + + public ACLEntryVoterException(String msg) + { + super(msg); + } + + public ACLEntryVoterException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterTest.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterTest.java new file mode 100644 index 0000000000..c399a5add2 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoterTest.java @@ -0,0 +1,805 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.repo.security.permissions.impl.AbstractPermissionTest; +import org.alfresco.repo.security.permissions.impl.SimplePermissionEntry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.aop.target.SingletonTargetSource; + +public class ACLEntryVoterTest extends AbstractPermissionTest +{ + + public ACLEntryVoterTest() + { + super(); + } + + public void testBasicDenyNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + try + { + method.invoke(proxy, new Object[] { rootNodeRef }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + try + { + method.invoke(proxy, new Object[] { systemNodeRef }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + // Check we are allowed access to deleted nodes .. + + nodeService.deleteNode(systemNodeRef); + + assertNull(method.invoke(proxy, new Object[] { systemNodeRef })); + + } + + + public void testBasicDenyStore() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneStoreRef", new Class[] { StoreRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + try + { + method.invoke(proxy, new Object[] { rootNodeRef.getStoreRef() }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + } + + public void testAllowNullNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null }); + + } + + public void testAllowNullStore() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneStoreRef", new Class[] { StoreRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null }); + + } + + public void testAllowNullParentOnRealChildAssoc() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_PARENT.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + + } + + public void testAllowNullParent() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_PARENT.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null }); + + } + + public void testAllowNullChild() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null }); + + } + + public void testBasicDenyChildAssocNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + try + { + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + } + + public void testBasicDenyParentAssocNode() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_PARENT.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + try + { + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + } + + public void testBasicAllowNode() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { rootNodeRef }); + } + + + public void testBasicAllow() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneNodeRef", new Class[] { NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_ALLOW"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { rootNodeRef }); + } + + public void testBasicAllowStore() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneStoreRef", new Class[] { StoreRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { rootNodeRef.getStoreRef() }); + } + + public void testBasicAllowChildAssocNode() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef) }); + } + + public void testBasicAllowParentAssocNode() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_PARENT.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + } + + public void testDenyParentAssocNode() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(systemNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_PARENT.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + try + { + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + } + + public void testAllowChildAssocNode() throws Exception + { + runAs("andy"); + + permissionService.setPermission(new SimplePermissionEntry(systemNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ_CHILDREN), "andy", + AccessStatus.ALLOWED)); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testOneChildAssociationRef", new Class[] { ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); + + } + + public void testMultiNodeMethodsArg0() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testManyNodeRef", + new Class[] { NodeRef.class, NodeRef.class, NodeRef.class, NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { rootNodeRef, null, null, null }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { rootNodeRef, null, null, null }); + } + + public void testMultiNodeMethodsArg1() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testManyNodeRef", + new Class[] { NodeRef.class, NodeRef.class, NodeRef.class, NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.1.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { null, rootNodeRef, null, null }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { null, rootNodeRef, null, null }); + } + + public void testMultiNodeMethodsArg2() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testManyNodeRef", + new Class[] { NodeRef.class, NodeRef.class, NodeRef.class, NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.2.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { null, null, rootNodeRef, null }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { null, null, rootNodeRef, null }); + } + + public void testMultiNodeMethodsArg3() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod("testManyNodeRef", + new Class[] { NodeRef.class, NodeRef.class, NodeRef.class, NodeRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.3.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { null, null, null, rootNodeRef }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { null, null, null, rootNodeRef }); + } + + public void testMultiChildAssocRefMethodsArg0() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod( + "testManyChildAssociationRef", + new Class[] { ChildAssociationRef.class, ChildAssociationRef.class, ChildAssociationRef.class, + ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.0.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef), null, null, null }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(rootNodeRef), null, null, null }); + } + + public void testMultiChildAssocRefMethodsArg1() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod( + "testManyChildAssociationRef", + new Class[] { ChildAssociationRef.class, ChildAssociationRef.class, ChildAssociationRef.class, + ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.1.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { null, nodeService.getPrimaryParent(rootNodeRef), null, null }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { null, nodeService.getPrimaryParent(rootNodeRef), null, null }); + } + + public void testMultiChildAssocRefMethodsArg2() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod( + "testManyChildAssociationRef", + new Class[] { ChildAssociationRef.class, ChildAssociationRef.class, ChildAssociationRef.class, + ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.2.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { null, null, nodeService.getPrimaryParent(rootNodeRef), null }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { null, null, nodeService.getPrimaryParent(rootNodeRef), null }); + } + + public void testMultiChildAssocRefMethodsArg3() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod( + "testManyChildAssociationRef", + new Class[] { ChildAssociationRef.class, ChildAssociationRef.class, ChildAssociationRef.class, + ChildAssociationRef.class }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_NODE.3.sys:base.Read"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { null, null, null, null }); + + try + { + method.invoke(proxy, new Object[] { null, null, null, nodeService.getPrimaryParent(rootNodeRef) }); + assertNotNull(null); + } + catch (InvocationTargetException e) + { + + } + + permissionService.setPermission(new SimplePermissionEntry(rootNodeRef, getPermission(PermissionService.READ), "andy", AccessStatus.ALLOWED)); + method.invoke(proxy, new Object[] { null, null, null, nodeService.getPrimaryParent(rootNodeRef) }); + } + + public void testMethodACL() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod( + "testMethod", + new Class[] { }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_METHOD.andy"))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { }); + } + + public void testMethodACL2() throws Exception + { + runAs("andy"); + + Object o = new ClassWithMethods(); + Method method = o.getClass().getMethod( + "testMethod", + new Class[] { }); + + AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addAdvisor(advisorAdapterRegistry.wrap(new Interceptor("ACL_METHOD."+PermissionService.ALL_AUTHORITIES))); + proxyFactory.setTargetSource(new SingletonTargetSource(o)); + Object proxy = proxyFactory.getProxy(); + + method.invoke(proxy, new Object[] { }); + } + + + public static class ClassWithMethods + { + public void testMethod() + { + + } + + public void testOneStoreRef(StoreRef storeRef) + { + + } + + public void testOneNodeRef(NodeRef nodeRef) + { + + } + + public void testManyNodeRef(NodeRef nodeRef1, NodeRef nodeRef2, NodeRef nodeRef3, NodeRef nodeRef4) + { + + } + + public void testOneChildAssociationRef(ChildAssociationRef car) + { + + } + + public void testManyChildAssociationRef(ChildAssociationRef car1, ChildAssociationRef car2, + ChildAssociationRef car3, ChildAssociationRef car4) + { + + } + } + + public class Interceptor implements MethodInterceptor + { + ConfigAttributeDefinition cad = new ConfigAttributeDefinition(); + + Interceptor(final String config) + { + cad.addConfigAttribute(new ConfigAttribute() + { + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 1L; + + public String getAttribute() + { + return config; + } + + }); + } + + public Object invoke(MethodInvocation invocation) throws Throwable + { + ACLEntryVoter voter = new ACLEntryVoter(); + voter.setNamespacePrefixResolver(namespacePrefixResolver); + voter.setPermissionService(permissionService); + voter.setNodeService(nodeService); + voter.setAuthenticationService(authenticationService); + voter.setAuthorityService(authorityService); + + if (!(voter.vote(null, invocation, cad) == AccessDecisionVoter.ACCESS_DENIED)) + { + return invocation.proceed(); + } + else + { + throw new ACLEntryVoterException("Access denied"); + } + + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSet.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSet.java new file mode 100644 index 0000000000..9d767d59d4 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSet.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.util.BitSet; +import java.util.List; +import java.util.ListIterator; + +import org.alfresco.repo.search.ResultSetRowIterator; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; + +public class FilteringResultSet extends ACLEntryAfterInvocationProvider implements ResultSet +{ + private ResultSet unfiltered; + + private BitSet inclusionMask; + + FilteringResultSet(ResultSet unfiltered) + { + super(); + this.unfiltered = unfiltered; + inclusionMask = new BitSet(unfiltered.length()); + } + + /* package */ResultSet getUnFilteredResultSet() + { + return unfiltered; + } + + /* package */void setIncluded(int i, boolean excluded) + { + inclusionMask.set(i, excluded); + } + + /* package */boolean getIncluded(int i) + { + return inclusionMask.get(i); + } + + public Path[] getPropertyPaths() + { + return unfiltered.getPropertyPaths(); + } + + public int length() + { + return inclusionMask.cardinality(); + } + + private int translateIndex(int n) + { + if (n > length()) + { + throw new IndexOutOfBoundsException(); + } + int count = -1; + for (int i = 0, l = unfiltered.length(); i < l; i++) + { + if (inclusionMask.get(i)) + { + count++; + } + if (count == n) + { + return i; + } + + } + throw new IndexOutOfBoundsException(); + } + + public NodeRef getNodeRef(int n) + { + return unfiltered.getNodeRef(translateIndex(n)); + } + + public float getScore(int n) + { + return unfiltered.getScore(translateIndex(n)); + } + + public void close() + { + unfiltered.close(); + } + + public ResultSetRow getRow(int i) + { + return unfiltered.getRow(translateIndex(i)); + } + + public List getNodeRefs() + { + List answer = unfiltered.getNodeRefs(); + for (int i = unfiltered.length() - 1; i >= 0; i--) + { + if (!inclusionMask.get(i)) + { + answer.remove(i); + } + } + return answer; + } + + public List getChildAssocRefs() + { + List answer = unfiltered.getChildAssocRefs(); + for (int i = unfiltered.length() - 1; i >= 0; i--) + { + if (!inclusionMask.get(i)) + { + answer.remove(i); + } + } + return answer; + } + + public ChildAssociationRef getChildAssocRef(int n) + { + return unfiltered.getChildAssocRef(translateIndex(n)); + } + + public ListIterator iterator() + { + return new FilteringIterator(); + } + + class FilteringIterator implements ResultSetRowIterator + { + // -1 at the start + int underlyingPosition = -1; + + public boolean hasNext() + { + return inclusionMask.nextSetBit(underlyingPosition + 1) != -1; + } + + public ResultSetRow next() + { + underlyingPosition = inclusionMask.nextSetBit(underlyingPosition + 1); + if( underlyingPosition == -1) + { + throw new IllegalStateException(); + } + return unfiltered.getRow(underlyingPosition); + } + + public boolean hasPrevious() + { + if (underlyingPosition <= 0) + { + return false; + } + else + { + for (int i = underlyingPosition - 1; i >= 0; i--) + { + if (inclusionMask.get(i)) + { + return true; + } + } + } + return false; + } + + public ResultSetRow previous() + { + if (underlyingPosition <= 0) + { + throw new IllegalStateException(); + } + for (int i = underlyingPosition - 1; i >= 0; i--) + { + if (inclusionMask.get(i)) + { + underlyingPosition = i; + return unfiltered.getRow(underlyingPosition); + } + } + throw new IllegalStateException(); + } + + public int nextIndex() + { + return inclusionMask.nextSetBit(underlyingPosition+1); + } + + public int previousIndex() + { + if (underlyingPosition <= 0) + { + return -1; + } + for (int i = underlyingPosition - 1; i >= 0; i--) + { + if (inclusionMask.get(i)) + { + return i; + } + } + return -1; + } + + /* + * Mutation is not supported + */ + + public void remove() + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void set(ResultSetRow o) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public void add(ResultSetRow o) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSetTest.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSetTest.java new file mode 100644 index 0000000000..f7c2822421 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/FilteringResultSetTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.util.ArrayList; +import java.util.ListIterator; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.results.ChildAssocRefResultSet; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.namespace.QName; + +public class FilteringResultSetTest extends TestCase +{ + + + + public FilteringResultSetTest() + { + super(); + } + + public FilteringResultSetTest(String arg0) + { + super(arg0); + } + + public void test() + { + StoreRef storeRef = new StoreRef("protocol", "test"); + NodeRef root = new NodeRef(storeRef, "n0"); + NodeRef n1 = new NodeRef(storeRef, "n1"); + NodeRef n2 = new NodeRef(storeRef, "n2"); + NodeRef n3 = new NodeRef(storeRef, "n3"); + NodeRef n4 = new NodeRef(storeRef, "n4"); + NodeRef n5 = new NodeRef(storeRef, "n5"); + + ArrayList cars = new ArrayList(); + ChildAssociationRef car0 = new ChildAssociationRef(null, null, null, root); + ChildAssociationRef car1 = new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, root, QName.createQName("{test}n2"), n1); + ChildAssociationRef car2 = new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, n1, QName.createQName("{test}n3"), n2); + ChildAssociationRef car3 = new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, n2, QName.createQName("{test}n4"), n3); + ChildAssociationRef car4 = new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, n3, QName.createQName("{test}n5"), n4); + ChildAssociationRef car5 = new ChildAssociationRef(ContentModel.ASSOC_CHILDREN, n4, QName.createQName("{test}n6"), n5); + cars.add(car0); + cars.add(car1); + cars.add(car2); + cars.add(car3); + cars.add(car4); + cars.add(car5); + + ResultSet in = new ChildAssocRefResultSet(null, cars, null); + + FilteringResultSet filtering = new FilteringResultSet(in); + + assertEquals(0, filtering.length()); + for(int i = 0; i < 6; i++) + { + filtering.setIncluded(i, true); + assertEquals(1, filtering.length()); + assertEquals("n"+i, filtering.getNodeRef(0).getId()); + filtering.setIncluded(i, false); + assertEquals(0, filtering.length()); + } + + for(int i = 0; i < 6; i++) + { + filtering.setIncluded(i, true); + assertEquals(i+1, filtering.length()); + assertEquals("n"+i, filtering.getNodeRef(i).getId()); + } + + int count = 0; + for(ResultSetRow row : filtering) + { + assertNotNull(row); + assertTrue(count < 6); + count++; + } + + ResultSetRow last = null; + for(ListIterator it = filtering.iterator(); it.hasNext(); /**/) + { + ResultSetRow row = it.next(); + if(last != null) + { + assertTrue(it.hasPrevious()); + ResultSetRow previous = it.previous(); + assertEquals(last.getIndex(), previous.getIndex()); + row = it.next(); + + } + else + { + assertFalse(it.hasPrevious()); + } + last = row; + + } + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityInterceptor.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityInterceptor.java new file mode 100644 index 0000000000..428147e2f3 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityInterceptor.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +public class MethodSecurityInterceptor extends + net.sf.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor +{ + + public MethodSecurityInterceptor() + { + super(); + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionTest.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionTest.java new file mode 100644 index 0000000000..cfbf56bc52 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.io.Serializable; + +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.util.BaseSpringTest; + +/** + * Test persistence and retrieval of Hibernate-specific implementations of the + * {@link org.alfresco.repo.domain.Node} interface + * + * @author Andy Hind + */ +public class HibernatePermissionTest extends BaseSpringTest +{ + + public HibernatePermissionTest() + { + } + + protected void onSetUpInTransaction() throws Exception + { + + } + + protected void onTearDownInTransaction() + { + // force a flush to ensure that the database updates succeed + getSession().flush(); + getSession().clear(); + } + + + public void testSimpleNodePermission() throws Exception + { + // create a new Node + NodePermissionEntry nodePermission = new NodePermissionEntryImpl(); + NodeKey key = new NodeKey("Random Protocol", "Random Identifier", "AAA"); + nodePermission.setNodeKey(key); + nodePermission.setInherits(true); + + Serializable id = getSession().save(nodePermission); + + // throw the reference away and get the a new one for the id + nodePermission = (NodePermissionEntry) getSession().load(NodePermissionEntryImpl.class, id); + assertNotNull("Node not found", nodePermission); + assertTrue(nodePermission.getInherits()); + + // Update inherits + + nodePermission.setInherits(false); + id = getSession().save(nodePermission); + + // throw the reference away and get the a new one for the id + nodePermission = (NodePermissionEntry) getSession().load(NodePermissionEntryImpl.class, id); + assertNotNull("Node not found", nodePermission); + assertFalse(nodePermission.getInherits()); + } + + public void testSimplePermissionReference() + { + PermissionReference permissionReference = new PermissionReferenceImpl(); + permissionReference.setName("Test"); + permissionReference.setTypeUri("TestUri"); + permissionReference.setTypeName("TestName"); + + Serializable id = getSession().save(permissionReference); + + // throw the reference away and get the a new one for the id + permissionReference = (PermissionReference) getSession().load(PermissionReferenceImpl.class, id); + assertNotNull("Node not found", permissionReference); + assertEquals("Test", permissionReference.getName()); + assertEquals("TestUri", permissionReference.getTypeUri()); + assertEquals("TestName", permissionReference.getTypeName()); + + // Test key + + PermissionReference key = new PermissionReferenceImpl(); + key.setName("Test"); + key.setTypeUri("TestUri"); + key.setTypeName("TestName"); + + permissionReference = (PermissionReference) getSession().load(PermissionReferenceImpl.class, key); + assertNotNull("Node not found", permissionReference); + assertEquals("Test", permissionReference.getName()); + assertEquals("TestUri", permissionReference.getTypeUri()); + assertEquals("TestName", permissionReference.getTypeName()); + } + + public void testSimpleRecipient() + { + Recipient recipient = new RecipientImpl(); + recipient.setRecipient("Test"); + recipient.getExternalKeys().add("One"); + + Serializable id = getSession().save(recipient); + + // throw the reference away and get the a new one for the id + recipient = (Recipient) getSession().load(RecipientImpl.class, id); + assertNotNull("Node not found", recipient); + assertEquals("Test", recipient.getRecipient()); + assertEquals(1, recipient.getExternalKeys().size()); + + // Key + + + Recipient key = new RecipientImpl(); + key.setRecipient("Test"); + + recipient = (Recipient) getSession().load(RecipientImpl.class, key); + assertNotNull("Node not found", recipient); + assertEquals("Test", recipient.getRecipient()); + assertEquals(1, recipient.getExternalKeys().size()); + + + // Update + + recipient.getExternalKeys().add("Two"); + id = getSession().save(recipient); + + // throw the reference away and get the a new one for the id + recipient = (Recipient) getSession().load(RecipientImpl.class, id); + assertNotNull("Node not found", recipient); + assertEquals("Test", recipient.getRecipient()); + assertEquals(2, recipient.getExternalKeys().size()); + + + // complex + + recipient.getExternalKeys().add("Three"); + recipient.getExternalKeys().remove("One"); + recipient.getExternalKeys().remove("Two"); + id = getSession().save(recipient); + + // Throw the reference away and get the a new one for the id + recipient = (Recipient) getSession().load(RecipientImpl.class, id); + assertNotNull("Node not found", recipient); + assertEquals("Test", recipient.getRecipient()); + assertEquals(1, recipient.getExternalKeys().size()); + + + } + + public void testNodePermissionEntry() + { + // create a new Node + NodePermissionEntry nodePermission = new NodePermissionEntryImpl(); + NodeKey key = new NodeKey("Random Protocol", "Random Identifier", "AAA"); + nodePermission.setNodeKey(key); + nodePermission.setInherits(true); + + Recipient recipient = new RecipientImpl(); + recipient.setRecipient("Test"); + recipient.getExternalKeys().add("One"); + + PermissionReference permissionReference = new PermissionReferenceImpl(); + permissionReference.setName("Test"); + permissionReference.setTypeUri("TestUri"); + permissionReference.setTypeName("TestName"); + + PermissionEntry permissionEntry = PermissionEntryImpl.create(nodePermission, permissionReference, recipient, true); + + Serializable idNodePermision = getSession().save(nodePermission); + getSession().save(recipient); + getSession().save(permissionReference); + Serializable idPermEnt = getSession().save(permissionEntry); + + permissionEntry = (PermissionEntry) getSession().load(PermissionEntryImpl.class, idPermEnt); + assertNotNull("Permission entry not found", permissionEntry); + assertTrue(permissionEntry.isAllowed()); + assertNotNull(permissionEntry.getNodePermissionEntry()); + assertTrue(permissionEntry.getNodePermissionEntry().getInherits()); + assertNotNull(permissionEntry.getPermissionReference()); + assertEquals("Test", permissionEntry.getPermissionReference().getName()); + assertNotNull(permissionEntry.getRecipient()); + assertEquals("Test", permissionEntry.getRecipient().getRecipient()); + assertEquals(1, permissionEntry.getRecipient().getExternalKeys().size()); + + // Check traversal down + + nodePermission = (NodePermissionEntry) getSession().load(NodePermissionEntryImpl.class, idNodePermision); + assertEquals(1, nodePermission.getPermissionEntries().size()); + + permissionEntry.delete(); + getSession().delete(permissionEntry); + + nodePermission = (NodePermissionEntry) getSession().load(NodePermissionEntryImpl.class, idNodePermision); + assertEquals(0, nodePermission.getPermissionEntries().size()); + + + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionsDAO.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionsDAO.java new file mode 100644 index 0000000000..de955ea7ae --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/HibernatePermissionsDAO.java @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.repo.security.permissions.NodePermissionEntry; +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.PermissionsDAO; +import org.alfresco.repo.security.permissions.impl.SimpleNodePermissionEntry; +import org.alfresco.repo.security.permissions.impl.SimplePermissionEntry; +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.QName; +import org.hibernate.ObjectDeletedException; +import org.hibernate.Query; +import org.hibernate.Session; +import org.springframework.dao.DataAccessException; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * Support for accessing persisted permission information. + * + * This class maps between persisted objects and the external API defined in the + * PermissionsDAO interface. + * + * @author andyh + */ +public class HibernatePermissionsDAO extends HibernateDaoSupport implements PermissionsDAO +{ + private SimpleCache nullPermissionCache; + + public HibernatePermissionsDAO() + { + super(); + + } + + public void setNullPermissionCache(SimpleCache nullPermissionCache) + { + this.nullPermissionCache = nullPermissionCache; + } + + public NodePermissionEntry getPermissions(NodeRef nodeRef) + { + // Create the object if it is not found. + // Null objects are not cached in hibernate + // If the object does not exist it will repeatedly query to check its + // non existence. + + NodePermissionEntry npe = nullPermissionCache.get(nodeRef); + if (npe != null) + { + return npe; + } + + npe = createSimpleNodePermissionEntry(getHibernateNodePermissionEntry(nodeRef, false)); + if (npe == null) + { + SimpleNodePermissionEntry snpe = new SimpleNodePermissionEntry(nodeRef, true, Collections + . emptySet()); + npe = snpe; + nullPermissionCache.put(nodeRef, snpe); + } + return npe; + } + + /** + * Get the persisted NodePermissionEntry + * + * @param nodeRef + * @param create - + * create the object if it is missing + * @return + */ + private org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry getHibernateNodePermissionEntry( + NodeRef nodeRef, boolean create) + { + // Build the key + NodeKey nodeKey = getNodeKey(nodeRef); + try + { + Object obj = getHibernateTemplate().get(NodePermissionEntryImpl.class, nodeKey); + // Create if required + if ((obj == null) && create) + { + NodePermissionEntryImpl entry = new NodePermissionEntryImpl(); + entry.setNodeKey(nodeKey); + entry.setInherits(true); + getHibernateTemplate().save(entry); + nullPermissionCache.remove(nodeRef); + return entry; + } + return (org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry) obj; + } + catch (DataAccessException e) + { + if (e.contains(ObjectDeletedException.class)) + { + // the object no loner exists + if (create) + { + NodePermissionEntryImpl entry = new NodePermissionEntryImpl(); + entry.setNodeKey(nodeKey); + entry.setInherits(true); + getHibernateTemplate().save(entry); + nullPermissionCache.remove(nodeRef); + return entry; + } + else + { + return null; + } + } + throw e; + } + } + + /** + * Get a node key from a node reference + * + * @param nodeRef + * @return + */ + private NodeKey getNodeKey(NodeRef nodeRef) + { + NodeKey nodeKey = new NodeKey(nodeRef.getStoreRef().getProtocol(), nodeRef.getStoreRef().getIdentifier(), + nodeRef.getId()); + return nodeKey; + } + + public void deletePermissions(NodeRef nodeRef) + { + org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry found = getHibernateNodePermissionEntry( + nodeRef, false); + if (found != null) + { + deleteHibernateNodePermissionEntry(found); + } + } + + private void deleteHibernateNodePermissionEntry( + org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry hibernateNodePermissionEntry) + { + deleteHibernatePermissionEntries(hibernateNodePermissionEntry.getPermissionEntries()); + getHibernateTemplate().delete(hibernateNodePermissionEntry); + } + + private void deleteHibernatePermissionEntries( + Set permissionEntries) + { + // Avoid concurrent access problems during deletion + Set copy = new HashSet(); + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry permissionEntry : copy) + { + deleteHibernatePermissionEntry(permissionEntry); + } + } + + private void deleteHibernatePermissionEntry( + org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry permissionEntry) + { + // Unhook bidirectoinal relationships + permissionEntry.delete(); + getHibernateTemplate().delete(permissionEntry); + } + + public void deletePermissions(NodePermissionEntry nodePermissionEntry) + { + deletePermissions(nodePermissionEntry.getNodeRef()); + } + + public void deletePermissions(PermissionEntry permissionEntry) + { + org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry found = getHibernateNodePermissionEntry( + permissionEntry.getNodeRef(), false); + if (found != null) + { + Set deletable = new HashSet(); + + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry current : found + .getPermissionEntries()) + { + if (permissionEntry.equals(createSimplePermissionEntry(current))) + { + deletable.add(current); + } + } + + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry current : deletable) + { + deleteHibernatePermissionEntry(current); + } + } + } + + public void clearPermission(NodeRef nodeRef, String authority) + { + org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry found = getHibernateNodePermissionEntry( + nodeRef, false); + if (found != null) + { + Set deletable = new HashSet(); + + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry current : found + .getPermissionEntries()) + { + if (createSimplePermissionEntry(current).getAuthority().equals(authority)) + { + deletable.add(current); + } + } + + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry current : deletable) + { + deleteHibernatePermissionEntry(current); + } + } + } + + public void deletePermissions(NodeRef nodeRef, String authority, PermissionReference perm, boolean allow) + { + SimplePermissionEntry spe = new SimplePermissionEntry(nodeRef, perm == null ? null + : new SimplePermissionReference(perm.getQName(), perm.getName()), authority, + allow ? AccessStatus.ALLOWED : AccessStatus.DENIED); + deletePermissions(spe); + } + + public void setPermission(NodeRef nodeRef, String authority, PermissionReference perm, boolean allow) + { + deletePermissions(nodeRef, authority, perm, allow); + PermissionEntryImpl entry = PermissionEntryImpl.create(getHibernateNodePermissionEntry(nodeRef, true), + getHibernatePermissionReference(perm, true), getHibernateAuthority(authority, true), allow); + getHibernateTemplate().save(entry); + nullPermissionCache.remove(nodeRef); + } + + /** + * Utility method to find or create a persisted authority + * + * @param authority + * @param create + * @return + */ + private Recipient getHibernateAuthority(String authority, boolean create) + { + Recipient key = new RecipientImpl(); + key.setRecipient(authority); + + Recipient found = (Recipient) getHibernateTemplate().get(RecipientImpl.class, key); + if ((found == null) && create) + { + getHibernateTemplate().save(key); + return key; + } + else + { + return found; + } + + } + + /** + * Utility method to find and optionally create a persisted permission + * reference. + * + * @param perm + * @param create + * @return + */ + private org.alfresco.repo.security.permissions.impl.hibernate.PermissionReference getHibernatePermissionReference( + PermissionReference perm, boolean create) + { + org.alfresco.repo.security.permissions.impl.hibernate.PermissionReference key = new PermissionReferenceImpl(); + key.setTypeUri(perm.getQName().getNamespaceURI()); + key.setTypeName(perm.getQName().getLocalName()); + key.setName(perm.getName()); + + org.alfresco.repo.security.permissions.impl.hibernate.PermissionReference found; + + found = (org.alfresco.repo.security.permissions.impl.hibernate.PermissionReference) getHibernateTemplate().get( + PermissionReferenceImpl.class, key); + if ((found == null) && create) + { + getHibernateTemplate().save(key); + return key; + } + else + { + return found; + } + + } + + public void setPermission(PermissionEntry permissionEntry) + { + setPermission(permissionEntry.getNodeRef(), permissionEntry.getAuthority(), permissionEntry + .getPermissionReference(), permissionEntry.isAllowed()); + } + + public void setPermission(NodePermissionEntry nodePermissionEntry) + { + deletePermissions(nodePermissionEntry); + NodePermissionEntryImpl entry = new NodePermissionEntryImpl(); + entry.setInherits(nodePermissionEntry.inheritPermissions()); + entry.setNodeKey(getNodeKey(nodePermissionEntry.getNodeRef())); + getHibernateTemplate().save(entry); + nullPermissionCache.remove(nodePermissionEntry.getNodeRef()); + for (PermissionEntry pe : nodePermissionEntry.getPermissionEntries()) + { + setPermission(pe); + } + } + + public void setInheritParentPermissions(NodeRef nodeRef, boolean inheritParentPermissions) + { + getHibernateNodePermissionEntry(nodeRef, true).setInherits(inheritParentPermissions); + } + + public boolean getInheritParentPermissions(NodeRef nodeRef) + { + return getHibernateNodePermissionEntry(nodeRef, true).getInherits(); + } + + @SuppressWarnings("unchecked") + public void deleteAllPermissionsForAuthority(final String authority) + { + + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery("permission.GetPermissionsForRecipient"); + query.setString("recipientKey", authority); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry current : queryResults) + { + deleteHibernatePermissionEntry(current); + } + + } + + // Utility methods to create simple detached objects for the outside + // // world + // We do not pass out the hibernate objects + + private static SimpleNodePermissionEntry createSimpleNodePermissionEntry( + org.alfresco.repo.security.permissions.impl.hibernate.NodePermissionEntry npe) + { + if (npe == null) + { + return null; + } + SimpleNodePermissionEntry snpe = new SimpleNodePermissionEntry(npe.getNodeRef(), npe.getInherits(), + createSimplePermissionEntries(npe.getPermissionEntries())); + return snpe; + } + + private static Set createSimplePermissionEntries( + Set nes) + { + if (nes == null) + { + return null; + } + HashSet spes = new HashSet(); + for (org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry pe : nes) + { + spes.add(createSimplePermissionEntry(pe)); + } + return spes; + } + + private static SimplePermissionEntry createSimplePermissionEntry( + org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntry pe) + { + if (pe == null) + { + return null; + } + return new SimplePermissionEntry(pe.getNodePermissionEntry().getNodeRef(), createSimplePermissionReference(pe + .getPermissionReference()), pe.getRecipient().getRecipient(), pe.isAllowed() ? AccessStatus.ALLOWED + : AccessStatus.DENIED); + } + + private static SimplePermissionReference createSimplePermissionReference( + org.alfresco.repo.security.permissions.impl.hibernate.PermissionReference pr) + { + if (pr == null) + { + return null; + } + return new SimplePermissionReference(QName.createQName(pr.getTypeUri(), pr.getTypeName()), pr.getName()); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntry.java new file mode 100644 index 0000000000..9f13d179a1 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntry.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.util.Set; + +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * The interface to support persistence of node permission entries in hibernate + * + * @author andyh + */ +public interface NodePermissionEntry +{ + /** + * Get the node key. + * + * @return + */ + public NodeKey getNodeKey(); + + /** + * Set the node key. + * + * @param key + */ + public void setNodeKey(NodeKey key); + + /** + * Get the node ref + * + * @return + */ + public NodeRef getNodeRef(); + + /** + * Get inheritance behaviour + * @return + */ + public boolean getInherits(); + + /** + * Set inheritance behaviour + * @param inherits + */ + public void setInherits(boolean inherits); + + /** + * Get the permission entries set for the node + * @return + */ + public Set getPermissionEntries(); + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntryImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntryImpl.java new file mode 100644 index 0000000000..da0bf1650c --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/NodePermissionEntryImpl.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.repo.domain.NodeKey; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * The hibernate persisted class for node permission entries. + * + * @author andyh + */ +public class NodePermissionEntryImpl implements NodePermissionEntry +{ + /** + * The key to find node permission entries + */ + private NodeKey nodeKey; + + /** + * Inherit permissions from the parent node? + */ + private boolean inherits; + + /** + * The set of permission entries. + */ + private Set permissionEntries = new HashSet(); + + public NodePermissionEntryImpl() + { + super(); + } + + public NodeKey getNodeKey() + { + return nodeKey; + } + + public void setNodeKey(NodeKey nodeKey) + { + this.nodeKey = nodeKey; + } + + public NodeRef getNodeRef() + { + return new NodeRef(new StoreRef(nodeKey.getProtocol(), nodeKey + .getIdentifier()), nodeKey.getGuid()); + } + + public boolean getInherits() + { + return inherits; + } + + public void setInherits(boolean inherits) + { + this.inherits = inherits; + } + + public Set getPermissionEntries() + { + return permissionEntries; + } + + // Hibernate + + /* package */ void setPermissionEntries(Set permissionEntries) + { + this.permissionEntries = permissionEntries; + } + + // Hibernate pattern + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof NodePermissionEntryImpl)) + { + return false; + } + NodePermissionEntryImpl other = (NodePermissionEntryImpl) o; + + return this.nodeKey.equals(other.nodeKey) + && (this.inherits == other.inherits) + && (this.permissionEntries.equals(other.permissionEntries)); + } + + @Override + public int hashCode() + { + return nodeKey.hashCode(); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/Permission.hbm.xml b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/Permission.hbm.xml new file mode 100644 index 0000000000..48d6a2e47a --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/Permission.hbm.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + permissionEntry + from + org.alfresco.repo.security.permissions.impl.hibernate.PermissionEntryImpl as permissionEntry + join permissionEntry.recipient as recipient + where + recipient = :recipientKey + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntry.java new file mode 100644 index 0000000000..36d09f8ff1 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntry.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +/** + * The interface against which permission entries are persisted + * + * @author andyh + */ + +public interface PermissionEntry +{ + /** + * Get the identifier for this object. + * + * @return + */ + public long getId(); + + /** + * Get the containing node permission entry. + * + * @return + */ + public NodePermissionEntry getNodePermissionEntry(); + + /** + * Get the permission to which this entry applies. + * + * @return + */ + public PermissionReference getPermissionReference(); + + /** + * Get the recipient to which this entry applies. + * + * @return + */ + public Recipient getRecipient(); + + /** + * Is this permission allowed? + * @return + */ + public boolean isAllowed(); + + /** + * Set if this permission is allowed, otherwise it is denied. + * + * @param allowed + */ + public void setAllowed(boolean allowed); + + /** + * Delete this permission entry - allows for deleting of the bidirectional relationship to the node permission entry. + * + */ + public void delete(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntryImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntryImpl.java new file mode 100644 index 0000000000..5ef6e1aaa7 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionEntryImpl.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import org.alfresco.util.EqualsHelper; + +/** + * Persisted permission entries + * + * @author andyh + */ +public class PermissionEntryImpl implements PermissionEntry +{ + /** + * The object id + */ + private long id; + + /** + * The container of this permissions + */ + private NodePermissionEntry nodePermissionEntry; + + /** + * The permission to which this applies + * (non null - all is a special string) + */ + private PermissionReference permissionReference; + + /** + * The recipient to which this applies + * (non null - all is a special string) + */ + private Recipient recipient; + + /** + * Is this permission allowed? + */ + private boolean allowed; + + public PermissionEntryImpl() + { + super(); + } + + public long getId() + { + return id; + } + + // Hibernate + + /* package */ void setId(long id) + { + this.id = id; + } + + public NodePermissionEntry getNodePermissionEntry() + { + return nodePermissionEntry; + } + + private void setNodePermissionEntry(NodePermissionEntry nodePermissionEntry) + { + this.nodePermissionEntry = nodePermissionEntry; + } + + public PermissionReference getPermissionReference() + { + return permissionReference; + } + + private void setPermissionReference(PermissionReference permissionReference) + { + this.permissionReference = permissionReference; + } + + public Recipient getRecipient() + { + return recipient; + } + + private void setRecipient(Recipient recipient) + { + this.recipient = recipient; + } + + public boolean isAllowed() + { + return allowed; + } + + public void setAllowed(boolean allowed) + { + this.allowed = allowed; + } + + + /** + * Factory method to create an entry and wire it in to the contained nodePermissionEntry + * + * @param nodePermissionEntry + * @param permissionReference + * @param recipient + * @param allowed + * @return + */ + public static PermissionEntryImpl create(NodePermissionEntry nodePermissionEntry, PermissionReference permissionReference, Recipient recipient, boolean allowed) + { + PermissionEntryImpl permissionEntry = new PermissionEntryImpl(); + permissionEntry.setNodePermissionEntry(nodePermissionEntry); + permissionEntry.setPermissionReference(permissionReference); + permissionEntry.setRecipient(recipient); + permissionEntry.setAllowed(allowed); + nodePermissionEntry.getPermissionEntries().add(permissionEntry); + return permissionEntry; + } + + /** + * Unwire + */ + public void delete() + { + nodePermissionEntry.getPermissionEntries().remove(this); + } + + // + // Hibernate object pattern + // + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof PermissionEntryImpl)) + { + return false; + } + PermissionEntryImpl other = (PermissionEntryImpl) o; + return EqualsHelper.nullSafeEquals(this.nodePermissionEntry, + other.nodePermissionEntry) + && EqualsHelper.nullSafeEquals(this.permissionReference, + other.permissionReference) + && EqualsHelper.nullSafeEquals(this.recipient, other.recipient) + && (this.allowed == other.allowed); + } + + @Override + public int hashCode() + { + int hashCode = nodePermissionEntry.hashCode(); + if (permissionReference != null) + { + hashCode = hashCode * 37 + permissionReference.hashCode(); + } + if (recipient != null) + { + hashCode = hashCode * 37 + recipient.hashCode(); + } + hashCode = hashCode * 37 + (allowed ? 1 : 0); + return hashCode; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReference.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReference.java new file mode 100644 index 0000000000..aa25962e36 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReference.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.io.Serializable; + +/** + * The interface against which permission references are persisted in hibernate. + * + * @author andyh + */ +public interface PermissionReference extends Serializable +{ + /** + * Get the URI for the type to which this permission applies. + * + * @return + */ + public String getTypeUri(); + + /** + * Set the URI for the type to which this permission applies. + * + * @param typeUri + */ + public void setTypeUri(String typeUri); + + /** + * Get the local name of the type to which this permission applies. + * + * @return + */ + public String getTypeName(); + + /** + * Set the local name of the type to which this permission applies. + * + * @param typeName + */ + public void setTypeName(String typeName); + + /** + * Get the name of the permission. + * + * @return + */ + public String getName(); + + /** + * Set the name of the permission. + * + * @param name + */ + public void setName(String name); +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReferenceImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReferenceImpl.java new file mode 100644 index 0000000000..65893116fa --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/PermissionReferenceImpl.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + + +/** + * The persisted class for permission references. + * + * @author andyh + */ +public class PermissionReferenceImpl implements PermissionReference +{ + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = -6352566900815035461L; + + private String typeUri; + + private String typeName; + + private String name; + + public PermissionReferenceImpl() + { + super(); + } + + public String getTypeUri() + { + return typeUri; + } + + public void setTypeUri(String typeUri) + { + this.typeUri = typeUri; + } + + public String getTypeName() + { + return typeName; + } + + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + // Hibernate pattern + + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(!(o instanceof PermissionReference)) + { + return false; + } + PermissionReference other = (PermissionReference)o; + return this.getTypeUri().equals(other.getTypeUri()) && this.getTypeName().equals(other.getTypeName()) && this.getName().equals(other.getName()); + } + + @Override + public int hashCode() + { + return ((typeUri.hashCode() * 37) + typeName.hashCode() ) * 37 + name.hashCode(); + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/Recipient.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/Recipient.java new file mode 100644 index 0000000000..47c0a56112 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/Recipient.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.io.Serializable; +import java.util.Set; + +/** + * The interface against which recipients of permission are persisted + * @author andyh + */ +public interface Recipient extends Serializable +{ + /** + * Get the recipient. + * + * @return + */ + public String getRecipient(); + + /** + * Set the recipient + * + * @param recipient + */ + public void setRecipient(String recipient); + + /** + * Get the external keys that map to this recipient. + * + * @return + */ + public Set getExternalKeys(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/hibernate/RecipientImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/RecipientImpl.java new file mode 100644 index 0000000000..8d12b6f396 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/hibernate/RecipientImpl.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.hibernate; + +import java.util.HashSet; +import java.util.Set; + +/** + * The persisted class for recipients. + * + * @author andyh + */ +public class RecipientImpl implements Recipient +{ + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = -5582068692208928127L; + + private String recipient; + + private Set externalKeys = new HashSet(); + + public RecipientImpl() + { + super(); + } + + public String getRecipient() + { + return recipient; + } + + public void setRecipient(String recipient) + { + this.recipient = recipient; + } + + public Set getExternalKeys() + { + return externalKeys; + } + + // Hibernate + /* package */ void setExternalKeys(Set externalKeys) + { + this.externalKeys = externalKeys; + } + + // Hibernate pattern + + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(!(o instanceof Recipient)) + { + return false; + } + Recipient other = (Recipient)o; + return this.getRecipient().equals(other.getRecipient()); + } + + @Override + public int hashCode() + { + return getRecipient().hashCode(); + } + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/AbstractPermission.java b/source/java/org/alfresco/repo/security/permissions/impl/model/AbstractPermission.java new file mode 100644 index 0000000000..b3842ee718 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/AbstractPermission.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.alfresco.repo.security.permissions.impl.AbstractPermissionReference; +import org.alfresco.repo.security.permissions.impl.RequiredPermission; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Attribute; +import org.dom4j.Element; + +/** + * Support to read and store common properties for permissions + * + * @author andyh + */ +public abstract class AbstractPermission extends AbstractPermissionReference implements XMLModelInitialisable +{ + /* XML Constants */ + + private static final String NAME = "name"; + + private static final String REQUIRED_PERMISSION = "requiredPermission"; + + private static final String RP_NAME = "name"; + + private static final String RP_TYPE = "type"; + + private static final String RP_ON = "on"; + + private static final String RP_IMPLIES = "implies"; + + private static final String NODE_ENTRY = "node"; + + private static final String PARENT_ENTRY = "parent"; + + private static final String CHILDREN_ENTRY = "children"; + + /* Instance variables */ + + private String name; + + private QName typeQName; + + private Set requiredPermissions = new HashSet(); + + public AbstractPermission(QName typeQName) + { + super(); + this.typeQName = typeQName; + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + name = element.attributeValue(NAME); + + for (Iterator rpit = element.elementIterator(REQUIRED_PERMISSION); rpit.hasNext(); /**/) + { + QName qName; + Element requiredPermissionElement = (Element) rpit.next(); + Attribute typeAttribute = requiredPermissionElement.attribute(RP_TYPE); + if (typeAttribute != null) + { + qName = QName.createQName(typeAttribute.getStringValue(), nspr); + } + else + { + qName = typeQName; + } + + String requiredName = requiredPermissionElement.attributeValue(RP_NAME); + + RequiredPermission.On on; + String onString = requiredPermissionElement.attributeValue(RP_ON); + if (onString.equalsIgnoreCase(NODE_ENTRY)) + { + on = RequiredPermission.On.NODE; + } + else if (onString.equalsIgnoreCase(PARENT_ENTRY)) + { + on = RequiredPermission.On.PARENT; + } + else if (onString.equalsIgnoreCase(CHILDREN_ENTRY)) + { + on = RequiredPermission.On.CHILDREN; + } + else + { + throw new PermissionModelException("Required permission must specify parent or node for the on attribute."); + } + + boolean implies = false; + Attribute impliesAttribute = requiredPermissionElement.attribute(RP_IMPLIES); + if( impliesAttribute != null) + { + implies = Boolean.parseBoolean(impliesAttribute.getStringValue()); + } + + RequiredPermission rq = new RequiredPermission(qName, requiredName, on, implies); + + requiredPermissions.add(rq); + + } + + } + + public String getName() + { + return name; + } + + public Set getRequiredPermissions() + { + return Collections.unmodifiableSet(requiredPermissions); + } + + public QName getTypeQName() + { + return typeQName; + } + + + public QName getQName() + { + return getTypeQName(); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/DynamicPermission.java b/source/java/org/alfresco/repo/security/permissions/impl/model/DynamicPermission.java new file mode 100644 index 0000000000..6769ee53e7 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/DynamicPermission.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Element; + +/** + * The definition of a required permission + * + * @author andyh + */ +public class DynamicPermission extends AbstractPermission implements XMLModelInitialisable +{ + private static final String EVALUATOR = "evaluator"; + + private String evaluatorFullyQualifiedClassName; + + public DynamicPermission(QName typeQName) + { + super(typeQName); + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + super.initialise(element, nspr, permissionModel); + evaluatorFullyQualifiedClassName = element.attributeValue(EVALUATOR); + } + + public String getEvaluatorFullyQualifiedClassName() + { + return evaluatorFullyQualifiedClassName; + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/GlobalPermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/model/GlobalPermissionEntry.java new file mode 100644 index 0000000000..810d0b96e0 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/GlobalPermissionEntry.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.dom4j.Attribute; +import org.dom4j.Element; + +public class GlobalPermissionEntry implements XMLModelInitialisable, PermissionEntry +{ + private static final String AUTHORITY = "authority"; + + private static final String PERMISSION = "permission"; + + private String authority; + + private PermissionReference permissionReference; + + public GlobalPermissionEntry() + { + super(); + // TODO Auto-generated constructor stub + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + Attribute authorityAttribute = element.attribute(AUTHORITY); + if(authorityAttribute != null) + { + authority = authorityAttribute.getStringValue(); + } + Attribute permissionAttribute = element.attribute(PERMISSION); + if(permissionAttribute != null) + { + permissionReference = permissionModel.getPermissionReference(null, permissionAttribute.getStringValue()); + } + + } + + public String getAuthority() + { + return authority; + } + + public PermissionReference getPermissionReference() + { + return permissionReference; + } + + public NodeRef getNodeRef() + { + return null; + } + + public boolean isDenied() + { + return false; + } + + public boolean isAllowed() + { + return true; + } + + public AccessStatus getAccessStatus() + { + return AccessStatus.ALLOWED; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/ModelPermissionEntry.java b/source/java/org/alfresco/repo/security/permissions/impl/model/ModelPermissionEntry.java new file mode 100644 index 0000000000..75c3b08ed2 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/ModelPermissionEntry.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.PermissionReferenceImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Attribute; +import org.dom4j.Element; + +/** + * Support to read and store the definion of a permission entry. + * + * @author andyh + */ +public class ModelPermissionEntry implements PermissionEntry, XMLModelInitialisable +{ + // XML Constants + + private static final String PERMISSION_REFERENCE = "permissionReference"; + + private static final String RECIPIENT = "recipient"; + + private static final String ACCESS = "access"; + + private static final String DENY = "deny"; + + private static final String ALLOW = "allow"; + + private static final String TYPE = "type"; + + private static final String NAME = "name"; + + // Instance variables + + private String recipient; + + private AccessStatus access; + + private PermissionReference permissionReference; + + private NodeRef nodeRef; + + public ModelPermissionEntry(NodeRef nodeRef) + { + super(); + this.nodeRef = nodeRef; + } + + public PermissionReference getPermissionReference() + { + return permissionReference; + } + + public String getAuthority() + { + return getRecipient(); + } + + public String getRecipient() + { + return recipient; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public boolean isDenied() + { + return access == AccessStatus.DENIED; + } + + public boolean isAllowed() + { + return access == AccessStatus.ALLOWED; + } + + public AccessStatus getAccessStatus() + { + return access; + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + Attribute recipientAttribute = element.attribute(RECIPIENT); + if (recipientAttribute != null) + { + recipient = recipientAttribute.getStringValue(); + } + else + { + recipient = null; + } + + Attribute accessAttribute = element.attribute(ACCESS); + if (accessAttribute != null) + { + if (accessAttribute.getStringValue().equalsIgnoreCase(ALLOW)) + { + access = AccessStatus.ALLOWED; + } + else if (accessAttribute.getStringValue().equalsIgnoreCase(DENY)) + { + access = AccessStatus.DENIED; + } + else + { + throw new PermissionModelException("The default permission must be deny or allow"); + } + } + else + { + access = AccessStatus.DENIED; + } + + + Element permissionReferenceElement = element.element(PERMISSION_REFERENCE); + QName typeQName = QName.createQName(permissionReferenceElement.attributeValue(TYPE), nspr); + String name = permissionReferenceElement.attributeValue(NAME); + permissionReference = new PermissionReferenceImpl(typeQName, name); + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/NodePermission.java b/source/java/org/alfresco/repo/security/permissions/impl/model/NodePermission.java new file mode 100644 index 0000000000..9de22dcfe9 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/NodePermission.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.alfresco.repo.security.permissions.NodePermissionEntry; +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.dom4j.Attribute; +import org.dom4j.Element; + +/** + * Support to read and store the definition of node permissions + * @author andyh + */ +public class NodePermission implements NodePermissionEntry, XMLModelInitialisable +{ + // XML Constants + + private static final String NODE_REF = "nodeRef"; + + private static final String NODE_PERMISSION = "nodePermission"; + + private static final String INHERIT_FROM_PARENT = "inheritFromParent"; + + // Instance variables + + // If null then it is the root. + private NodeRef nodeRef; + + private Set permissionEntries = new HashSet(); + + private boolean inheritPermissionsFromParent; + + public NodePermission() + { + super(); + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public boolean inheritPermissions() + { + return inheritPermissionsFromParent; + } + + public Set getPermissionEntries() + { + return Collections.unmodifiableSet(permissionEntries); + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + Attribute nodeRefAttribute = element.attribute(NODE_REF); + if(nodeRefAttribute != null) + { + nodeRef = new NodeRef(nodeRefAttribute.getStringValue()); + } + + Attribute inheritFromParentAttribute = element.attribute(INHERIT_FROM_PARENT); + if(inheritFromParentAttribute != null) + { + inheritPermissionsFromParent = Boolean.parseBoolean(inheritFromParentAttribute.getStringValue()); + } + else + { + inheritPermissionsFromParent = true; + } + + // Node Permissions Entry + + for (Iterator npit = element.elementIterator(NODE_PERMISSION); npit.hasNext(); /**/) + { + Element permissionEntryElement = (Element) npit.next(); + ModelPermissionEntry permissionEntry = new ModelPermissionEntry(nodeRef); + permissionEntry.initialise(permissionEntryElement, nspr, permissionModel); + permissionEntries.add(permissionEntry); + } + + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/Permission.java b/source/java/org/alfresco/repo/security/permissions/impl/model/Permission.java new file mode 100644 index 0000000000..b3ca8e4c14 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/Permission.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.PermissionReferenceImpl; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Attribute; +import org.dom4j.Element; + +/** + * Support to read and store the definition of a permission. + * + * @author andyh + */ +public class Permission extends AbstractPermission implements XMLModelInitialisable +{ + // XML Constants + + private static final String GRANTED_TO_GROUP = "grantedToGroup"; + + private static final String GTG_NAME = "permissionGroup"; + + private static final String GTG_TYPE = "type"; + + private Set grantedToGroups = new HashSet(); + + private static final String DENY = "deny"; + + private static final String ALLOW = "allow"; + + private static final String DEFAULT_PERMISSION = "defaultPermission"; + + private static final String EXPOSE = "expose"; + + private static final String REQUIRES_TYPE = "requiresType"; + + private AccessStatus defaultPermission; + + private boolean isExposed; + + private boolean requiresType; + + public Permission(QName typeQName) + { + super(typeQName); + + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + super.initialise(element, nspr, permissionModel); + + Attribute att = element.attribute(EXPOSE); + if (att != null) + { + isExposed = Boolean.parseBoolean(att.getStringValue()); + } + else + { + isExposed = true; + } + + att = element.attribute(REQUIRES_TYPE); + if (att != null) + { + requiresType = Boolean.parseBoolean(att.getStringValue()); + } + else + { + requiresType = true; + } + + Attribute defaultPermissionAttribute = element.attribute(DEFAULT_PERMISSION); + if(defaultPermissionAttribute != null) + { + if(defaultPermissionAttribute.getStringValue().equalsIgnoreCase(ALLOW)) + { + defaultPermission = AccessStatus.ALLOWED; + } + else if(defaultPermissionAttribute.getStringValue().equalsIgnoreCase(DENY)) + { + defaultPermission = AccessStatus.DENIED; + } + else + { + throw new PermissionModelException("The default permission must be deny or allow"); + } + } + else + { + defaultPermission = AccessStatus.DENIED; + } + + for (Iterator gtgit = element.elementIterator(GRANTED_TO_GROUP); gtgit.hasNext(); /**/) + { + QName qName; + Element grantedToGroupsElement = (Element) gtgit.next(); + Attribute typeAttribute = grantedToGroupsElement.attribute(GTG_TYPE); + if (typeAttribute != null) + { + qName = QName.createQName(typeAttribute.getStringValue(), nspr); + } + else + { + qName = getTypeQName(); + } + + String grantedName = grantedToGroupsElement.attributeValue(GTG_NAME); + + grantedToGroups.add(new PermissionReferenceImpl(qName, grantedName)); + } + + } + + public AccessStatus getDefaultPermission() + { + return defaultPermission; + } + + public Set getGrantedToGroups() + { + return Collections.unmodifiableSet(grantedToGroups); + } + + public boolean isExposed() + { + return isExposed; + } + + public boolean isTypeRequired() + { + return requiresType; + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionGroup.java b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionGroup.java new file mode 100644 index 0000000000..e2f8a4b525 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionGroup.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.AbstractPermissionReference; +import org.alfresco.repo.security.permissions.impl.PermissionReferenceImpl; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Attribute; +import org.dom4j.Element; + +/** + * Support to read and store the defintion of permission groups. + * + * @author andyh + */ +public class PermissionGroup extends AbstractPermissionReference implements XMLModelInitialisable +{ + // XML Constants + + private static final String NAME = "name"; + + private static final String EXTENDS = "extends"; + + private static final String ALLOW_FULL_CONTOL = "allowFullControl"; + + private static final String INCLUDE_PERMISSION_GROUP = "includePermissionGroup"; + + private static final String PERMISSION_GROUP = "permissionGroup"; + + private static final String TYPE = "type"; + + private static final String EXPOSE = "expose"; + + private static final String REQUIRES_TYPE = "requiresType"; + + private String name; + + private QName type; + + private boolean extendz; + + private boolean isExposed; + + private boolean allowFullControl; + + private QName container; + + private Set includedPermissionGroups = new HashSet(); + + private boolean requiresType; + + public PermissionGroup(QName container) + { + super(); + this.container = container; + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + // Name + name = element.attributeValue(NAME); + // Allow full control + Attribute att = element.attribute(ALLOW_FULL_CONTOL); + if (att != null) + { + allowFullControl = Boolean.parseBoolean(att.getStringValue()); + } + else + { + allowFullControl = false; + } + + att = element.attribute(REQUIRES_TYPE); + if (att != null) + { + requiresType = Boolean.parseBoolean(att.getStringValue()); + } + else + { + requiresType = true; + } + + att = element.attribute(EXTENDS); + if (att != null) + { + extendz = Boolean.parseBoolean(att.getStringValue()); + } + else + { + extendz = false; + } + + att = element.attribute(EXPOSE); + if (att != null) + { + isExposed = Boolean.parseBoolean(att.getStringValue()); + } + else + { + isExposed = true; + } + + att = element.attribute(TYPE); + if (att != null) + { + type = QName.createQName(att.getStringValue(),nspr); + } + else + { + type = null; + } + + // Include permissions defined for other permission groups + + for (Iterator ipgit = element.elementIterator(INCLUDE_PERMISSION_GROUP); ipgit.hasNext(); /**/) + { + QName qName; + Element includePermissionGroupElement = (Element) ipgit.next(); + Attribute typeAttribute = includePermissionGroupElement.attribute(TYPE); + if (typeAttribute != null) + { + qName = QName.createQName(typeAttribute.getStringValue(), nspr); + } + else + { + qName = container; + } + String refName = includePermissionGroupElement.attributeValue(PERMISSION_GROUP); + PermissionReference permissionReference = new PermissionReferenceImpl(qName, refName); + includedPermissionGroups.add(permissionReference); + } + } + + public Set getIncludedPermissionGroups() + { + return Collections.unmodifiableSet(includedPermissionGroups); + } + + public String getName() + { + return name; + } + + public boolean isAllowFullControl() + { + return allowFullControl; + } + + public QName getQName() + { + return container; + } + + public boolean isExtends() + { + return extendz; + } + + public QName getTypeQName() + { + return type; + } + + public boolean isExposed() + { + return isExposed; + } + + + public boolean isTypeRequired() + { + return requiresType; + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java new file mode 100644 index 0000000000..3fab3584d6 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java @@ -0,0 +1,944 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.ModelDAO; +import org.alfresco.repo.security.permissions.impl.RequiredPermission; +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.DynamicNamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.beans.factory.InitializingBean; + +/** + * The implementation of the model DAO + * + * Reads and stores the top level model information + * + * Encapsulates access to this information + * + * @author andyh + */ +public class PermissionModel implements ModelDAO, InitializingBean +{ + // IOC + + private NodeService nodeService; + + private DictionaryService dictionaryService; + + // XML Constants + + private static final String NAMESPACES = "namespaces"; + + private static final String NAMESPACE = "namespace"; + + private static final String NAMESPACE_URI = "uri"; + + private static final String NAMESPACE_PREFIX = "prefix"; + + private static final String PERMISSION_SET = "permissionSet"; + + private static final String GLOBAL_PERMISSION = "globalPermission"; + + private static final String DENY = "deny"; + + private static final String ALLOW = "allow"; + + private static final String DEFAULT_PERMISSION = "defaultPermission"; + + // Instance variables + + private String model; + + private Map permissionSets = new HashMap(); + + private Set globalPermissions = new HashSet(); + + private AccessStatus defaultPermission; + + // Cache granting permissions + private HashMap> grantingPermissions = new HashMap>(); + + // Cache grantees + private HashMap> granteePermissions = new HashMap>(); + + // Cache the mapping of extended groups to the base + private HashMap groupsToBaseGroup = new HashMap(); + + private HashMap uniqueMap; + + private HashMap permissionMap; + + private HashMap permissionGroupMap; + + private HashMap permissionReferenceMap; + + public PermissionModel() + { + super(); + } + + // IOC + + public void setModel(String model) + { + this.model = model; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /* + * Initialise from file + * + * (non-Javadoc) + * + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + + public void afterPropertiesSet() + { + Document document = createDocument(model); + Element root = document.getRootElement(); + + Attribute defaultPermissionAttribute = root.attribute(DEFAULT_PERMISSION); + if (defaultPermissionAttribute != null) + { + if (defaultPermissionAttribute.getStringValue().equalsIgnoreCase(ALLOW)) + { + defaultPermission = AccessStatus.ALLOWED; + } + else if (defaultPermissionAttribute.getStringValue().equalsIgnoreCase(DENY)) + { + defaultPermission = AccessStatus.DENIED; + } + else + { + throw new PermissionModelException("The default permission must be deny or allow"); + } + } + else + { + defaultPermission = AccessStatus.DENIED; + } + + DynamicNamespacePrefixResolver nspr = new DynamicNamespacePrefixResolver(); + + // Namespaces + + for (Iterator nsit = root.elementIterator(NAMESPACES); nsit.hasNext(); /**/) + { + Element namespacesElement = (Element) nsit.next(); + for (Iterator it = namespacesElement.elementIterator(NAMESPACE); it.hasNext(); /**/) + { + Element nameSpaceElement = (Element) it.next(); + nspr.registerNamespace(nameSpaceElement.attributeValue(NAMESPACE_PREFIX), nameSpaceElement + .attributeValue(NAMESPACE_URI)); + } + } + + // Permission Sets + + for (Iterator psit = root.elementIterator(PERMISSION_SET); psit.hasNext(); /**/) + { + Element permissionSetElement = (Element) psit.next(); + PermissionSet permissionSet = new PermissionSet(); + permissionSet.initialise(permissionSetElement, nspr, this); + + permissionSets.put(permissionSet.getQName(), permissionSet); + } + + buildUniquePermissionMap(); + + // NodePermissions + + for (Iterator npit = root.elementIterator(GLOBAL_PERMISSION); npit.hasNext(); /**/) + { + Element globalPermissionElement = (Element) npit.next(); + GlobalPermissionEntry globalPermission = new GlobalPermissionEntry(); + globalPermission.initialise(globalPermissionElement, nspr, this); + + globalPermissions.add(globalPermission); + } + + } + + /* + * Create the XML document from the file location + */ + private Document createDocument(String model) + { + InputStream is = this.getClass().getClassLoader().getResourceAsStream(model); + if (is == null) + { + throw new PermissionModelException("File not found: " + model); + } + SAXReader reader = new SAXReader(); + try + { + Document document = reader.read(is); + is.close(); + return document; + } + catch (DocumentException e) + { + throw new PermissionModelException("Failed to create permission model document ", e); + } + catch (IOException e) + { + throw new PermissionModelException("Failed to close permission model document ", e); + } + + } + + public AccessStatus getDefaultPermission() + { + return defaultPermission; + } + + public AccessStatus getDefaultPermission(PermissionReference pr) + { + Permission p = permissionMap.get(pr); + if (p == null) + { + return defaultPermission; + } + else + { + return p.getDefaultPermission(); + } + } + + public Set getGlobalPermissionEntries() + { + return Collections.unmodifiableSet(globalPermissions); + } + + public Map getPermissionSets() + { + return Collections.unmodifiableMap(permissionSets); + } + + public Set getAllPermissions(QName type) + { + return getAllPermissionsImpl(type, false); + } + + public Set getExposedPermissions(QName type) + { + return getAllPermissionsImpl(type, true); + } + + private Set getAllPermissionsImpl(QName type, boolean exposedOnly) + { + Set permissions = new HashSet(); + if (dictionaryService.getClass(type).isAspect()) + { + addAspectPermissions(type, permissions, exposedOnly); + } + else + { + mergeGeneralAspectPermissions(permissions, exposedOnly); + addTypePermissions(type, permissions, exposedOnly); + } + return permissions; + } + + /** + * Support to add permissions for types + * + * @param type + * @param permissions + */ + private void addTypePermissions(QName type, Set permissions, boolean exposedOnly) + { + TypeDefinition typeDef = dictionaryService.getType(type); + if (typeDef.getParentName() != null) + { + PermissionSet permissionSet = permissionSets.get(type); + if (!exposedOnly || (permissionSet == null) || permissionSet.exposeAll()) + { + addTypePermissions(typeDef.getParentName(), permissions, exposedOnly); + } + } + for (AspectDefinition ad : typeDef.getDefaultAspects()) + { + addAspectPermissions(ad.getName(), permissions, exposedOnly); + } + mergePermissions(permissions, type, exposedOnly, true); + } + + /** + * Support to add permissions for aspects. + * + * @param type + * @param permissions + */ + private void addAspectPermissions(QName type, Set permissions, boolean exposedOnly) + { + AspectDefinition aspectDef = dictionaryService.getAspect(type); + if (aspectDef.getParentName() != null) + { + PermissionSet permissionSet = permissionSets.get(type); + if (!exposedOnly || (permissionSet == null) || permissionSet.exposeAll()) + { + addAspectPermissions(aspectDef.getParentName(), permissions, exposedOnly); + } + } + mergePermissions(permissions, type, exposedOnly, true); + } + + /** + * Support to merge permissions together. Respects extended permissions. + * + * @param target + * @param type + */ + private void mergePermissions(Set target, QName type, boolean exposedOnly, boolean typeRequired) + { + PermissionSet permissionSet = permissionSets.get(type); + if (permissionSet != null) + { + for (PermissionGroup pg : permissionSet.getPermissionGroups()) + { + if (!exposedOnly || permissionSet.exposeAll() || pg.isExposed()) + { + if (!pg.isExtends()) + { + if (pg.isTypeRequired() == typeRequired) + { + target.add(pg); + } + } + else if (exposedOnly) + { + if (pg.isTypeRequired() == typeRequired) + { + target.add(getBasePermissionGroup(pg)); + } + } + } + } + for (Permission p : permissionSet.getPermissions()) + { + if (!exposedOnly || permissionSet.exposeAll() || p.isExposed()) + { + if (p.isTypeRequired() == typeRequired) + { + target.add(p); + } + } + } + } + } + + + private void mergeGeneralAspectPermissions(Set target, boolean exposedOnly) + { + for(QName aspect : dictionaryService.getAllAspects()) + { + mergePermissions(target, aspect, exposedOnly, false); + } + } + + public Set getAllPermissions(NodeRef nodeRef) + { + return getExposedPermissionsImpl(nodeRef, false); + } + + public Set getExposedPermissions(NodeRef nodeRef) + { + return getExposedPermissionsImpl(nodeRef, true); + } + + public Set getExposedPermissionsImpl(NodeRef nodeRef, boolean exposedOnly) + { + + QName typeName = nodeService.getType(nodeRef); + Set permissions = getAllPermissions(typeName); + mergeGeneralAspectPermissions(permissions, exposedOnly); + // Add non mandatory aspects.. + Set defaultAspects = new HashSet(); + for (AspectDefinition aspDef : dictionaryService.getType(typeName).getDefaultAspects()) + { + defaultAspects.add(aspDef.getName()); + } + for (QName aspect : nodeService.getAspects(nodeRef)) + { + if (!defaultAspects.contains(aspect)) + { + addAspectPermissions(aspect, permissions, exposedOnly); + } + } + return permissions; + + } + + public synchronized Set getGrantingPermissions(PermissionReference permissionReference) + { + // Cache the results + Set granters = grantingPermissions.get(permissionReference); + if (granters == null) + { + granters = getGrantingPermissionsImpl(permissionReference); + grantingPermissions.put(permissionReference, granters); + } + return granters; + } + + private Set getGrantingPermissionsImpl(PermissionReference permissionReference) + { + // Query the model + HashSet permissions = new HashSet(); + permissions.add(permissionReference); + for (PermissionSet ps : permissionSets.values()) + { + for (PermissionGroup pg : ps.getPermissionGroups()) + { + if (grants(pg, permissionReference)) + { + permissions.add(getBasePermissionGroup(pg)); + } + if (pg.isAllowFullControl()) + { + permissions.add(pg); + } + } + for (Permission p : ps.getPermissions()) + { + if (p.equals(permissionReference)) + { + for (PermissionReference pg : p.getGrantedToGroups()) + { + permissions.add(getBasePermissionGroup(getPermissionGroup(pg))); + } + } + for (RequiredPermission rp : p.getRequiredPermissions()) + { + if (rp.equals(permissionReference) && rp.isImplies()) + { + permissions.add(p); + break; + } + } + } + } + return permissions; + } + + private boolean grants(PermissionGroup pg, PermissionReference permissionReference) + { + if (pg.getIncludedPermissionGroups().contains(permissionReference)) + { + return true; + } + if (getGranteePermissions(pg).contains(permissionReference)) + { + return true; + } + for (PermissionReference nested : pg.getIncludedPermissionGroups()) + { + if (grants(getPermissionGroup(nested), permissionReference)) + { + return true; + } + } + return false; + } + + public synchronized Set getGranteePermissions(PermissionReference permissionReference) + { + // Cache the results + Set grantees = granteePermissions.get(permissionReference); + if (grantees == null) + { + grantees = getGranteePermissionsImpl(permissionReference); + granteePermissions.put(permissionReference, grantees); + } + return grantees; + } + + private Set getGranteePermissionsImpl(PermissionReference permissionReference) + { + // Query the model + HashSet permissions = new HashSet(); + permissions.add(permissionReference); + for (PermissionSet ps : permissionSets.values()) + { + for (PermissionGroup pg : ps.getPermissionGroups()) + { + if (pg.equals(permissionReference)) + { + for (PermissionReference included : pg.getIncludedPermissionGroups()) + { + permissions.addAll(getGranteePermissions(included)); + } + + if (pg.isExtends()) + { + if (pg.getTypeQName() != null) + { + permissions.addAll(getGranteePermissions(new SimplePermissionReference(pg.getTypeQName(), + pg.getName()))); + } + else + { + ClassDefinition classDefinition = dictionaryService.getClass(pg.getQName()); + QName parent = classDefinition.getParentName(); + if (parent != null) + { + classDefinition = dictionaryService.getClass(parent); + PermissionGroup attempt = getPermissionGroupOrNull(new SimplePermissionReference( + parent, pg.getName())); + if (attempt != null) + { + permissions.addAll(getGranteePermissions(attempt)); + } + } + } + } + + if (pg.isAllowFullControl()) + { + // add all available + permissions.addAll(getAllPermissions()); + } + } + } + PermissionGroup baseGroup = getBasePermissionGroupOrNull(getPermissionGroupOrNull(permissionReference)); + if (baseGroup != null) + { + for (Permission p : ps.getPermissions()) + { + for (PermissionReference grantedTo : p.getGrantedToGroups()) + { + PermissionGroup base = getBasePermissionGroupOrNull(getPermissionGroupOrNull(grantedTo)); + if (baseGroup.equals(base)) + { + permissions.add(p); + } + } + } + } + } + return permissions; + } + + private Set getAllPermissions() + { + HashSet permissions = new HashSet(); + for (PermissionSet ps : permissionSets.values()) + { + for (PermissionGroup pg : ps.getPermissionGroups()) + { + permissions.add(pg); + } + for (Permission p : ps.getPermissions()) + { + permissions.add(p); + } + } + return permissions; + } + + /** + * Support to find permission groups + * + * @param target + * @return + */ + private PermissionGroup getPermissionGroupOrNull(PermissionReference target) + { + PermissionGroup pg = permissionGroupMap.get(target); + return pg == null ? null : pg; + } + + /** + * Support to get a permission group + * + * @param target + * @return + */ + private PermissionGroup getPermissionGroup(PermissionReference target) + { + PermissionGroup pg = getPermissionGroupOrNull(target); + if (pg == null) + { + throw new PermissionModelException("There is no permission group :" + + target.getQName() + " " + target.getName()); + } + return pg; + } + + /** + * Get the base permission group for a given permission group. + * + * @param pg + * @return + */ + private synchronized PermissionGroup getBasePermissionGroupOrNull(PermissionGroup pg) + { + if (groupsToBaseGroup.containsKey(pg)) + { + return groupsToBaseGroup.get(pg); + } + else + { + PermissionGroup answer = getBasePermissionGroupOrNullImpl(pg); + groupsToBaseGroup.put(pg, answer); + return answer; + } + } + + /** + * Query the model for a base permission group + * + * Uses the Data Dictionary to reolve inheritance + * + * @param pg + * @return + */ + private PermissionGroup getBasePermissionGroupOrNullImpl(PermissionGroup pg) + { + if (pg == null) + { + return null; + } + if (pg.isExtends()) + { + if (pg.getTypeQName() != null) + { + return getPermissionGroup(new SimplePermissionReference(pg.getTypeQName(), pg.getName())); + } + else + { + ClassDefinition classDefinition = dictionaryService.getClass(pg.getQName()); + QName parent; + while ((parent = classDefinition.getParentName()) != null) + { + classDefinition = dictionaryService.getClass(parent); + PermissionGroup attempt = getPermissionGroupOrNull(new SimplePermissionReference(parent, pg + .getName())); + if ((attempt != null) && (!attempt.isExtends())) + { + return attempt; + } + } + return null; + } + } + else + { + return pg; + } + } + + private PermissionGroup getBasePermissionGroup(PermissionGroup target) + { + PermissionGroup pg = getBasePermissionGroupOrNull(target); + if (pg == null) + { + throw new PermissionModelException("There is no parent for permission group :" + + target.getQName() + " " + target.getName()); + } + return pg; + } + + public Set getRequiredPermissions(PermissionReference required, QName qName, + Set aspectQNames, RequiredPermission.On on) + { + PermissionGroup pg = getBasePermissionGroupOrNull(getPermissionGroupOrNull(required)); + if (pg == null) + { + return getRequirementsForPermission(required, on); + } + else + { + return getRequirementsForPermissionGroup(pg, on, qName, aspectQNames); + } + } + + /** + * Get the requirements for a permission + * + * @param required + * @param on + * @return + */ + private Set getRequirementsForPermission(PermissionReference required, RequiredPermission.On on) + { + HashSet requiredPermissions = new HashSet(); + Permission p = getPermissionOrNull(required); + if (p != null) + { + for (RequiredPermission rp : p.getRequiredPermissions()) + { + if (!rp.isImplies() && rp.getOn().equals(on)) + { + requiredPermissions.add(rp); + } + } + } + return requiredPermissions; + } + + /** + * Get the requirements for a permission set + * + * @param target + * @param on + * @param qName + * @param aspectQNames + * @return + */ + private Set getRequirementsForPermissionGroup(PermissionGroup target, + RequiredPermission.On on, QName qName, Set aspectQNames) + { + HashSet requiredPermissions = new HashSet(); + if (target == null) + { + return requiredPermissions; + } + for (PermissionSet ps : permissionSets.values()) + { + for (PermissionGroup pg : ps.getPermissionGroups()) + { + if (target.equals(getBasePermissionGroupOrNull(pg)) + && isPartOfDynamicPermissionGroup(pg, qName, aspectQNames)) + { + // Add includes + for (PermissionReference pr : pg.getIncludedPermissionGroups()) + { + requiredPermissions.addAll(getRequirementsForPermissionGroup( + getBasePermissionGroupOrNull(getPermissionGroupOrNull(pr)), on, qName, aspectQNames)); + } + } + } + for (Permission p : ps.getPermissions()) + { + for (PermissionReference grantedTo : p.getGrantedToGroups()) + { + PermissionGroup base = getBasePermissionGroupOrNull(getPermissionGroupOrNull(grantedTo)); + if (target.equals(base) && (!base.isTypeRequired() || isPartOfDynamicPermissionGroup(grantedTo, qName, aspectQNames))) + { + if (on == RequiredPermission.On.NODE) + { + requiredPermissions.add(p); + } + } + } + } + } + return requiredPermissions; + } + + /** + * Check type specifc extension of permission sets. + * + * @param pr + * @param typeQname + * @param aspects + * @return + */ + private boolean isPartOfDynamicPermissionGroup(PermissionReference pr, QName typeQname, Set aspects) + { + if (dictionaryService.isSubClass(typeQname, pr.getQName())) + { + return true; + } + for (QName aspect : aspects) + { + if (dictionaryService.isSubClass(aspect, pr.getQName())) + { + return true; + } + } + return false; + } + + /** + * Utility method to find a permission + * + * @param perm + * @return + */ + private Permission getPermissionOrNull(PermissionReference perm) + { + Permission p = permissionMap.get(perm); + return p == null ? null : p; + } + + public boolean checkPermission(PermissionReference required) + { + Permission permission = getPermissionOrNull(required); + if (permission != null) + { + return true; + } + PermissionGroup pg = getPermissionGroupOrNull(required); + if (pg != null) + { + if (pg.isExtends()) + { + if (pg.getTypeQName() != null) + { + return checkPermission(new SimplePermissionReference(pg.getTypeQName(), pg.getName())); + } + else + { + ClassDefinition classDefinition = dictionaryService.getClass(pg.getQName()); + QName parent; + while ((parent = classDefinition.getParentName()) != null) + { + classDefinition = dictionaryService.getClass(parent); + PermissionGroup attempt = getPermissionGroupOrNull(new SimplePermissionReference(parent, pg + .getName())); + if ((attempt != null) && attempt.isAllowFullControl()) + { + return true; + } + } + return false; + } + } + else + { + return pg.isAllowFullControl(); + } + } + else + { + return false; + } + + } + + public PermissionReference getPermissionReference(QName qname, String permissionName) + { + if(permissionName == null) + { + return null; + } + PermissionReference pr = uniqueMap.get(permissionName); + if (pr == null) + { + pr = permissionReferenceMap.get(permissionName); + if (pr == null) + { + throw new UnsupportedOperationException("Can not find " + permissionName); + } + } + return pr; + + } + + public boolean isUnique(PermissionReference permissionReference) + { + return uniqueMap.containsKey(permissionReference.getName()); + } + + private void buildUniquePermissionMap() + { + Set excluded = new HashSet(); + uniqueMap = new HashMap(); + permissionReferenceMap = new HashMap(); + permissionGroupMap = new HashMap(); + permissionMap = new HashMap(); + for (PermissionSet ps : permissionSets.values()) + { + for (PermissionGroup pg : ps.getPermissionGroups()) + { + if (uniqueMap.containsKey(pg.getName()) && !excluded.contains(pg.getName())) + { + PermissionReference value = uniqueMap.get(pg.getName()); + if (!value.equals(getBasePermissionGroup(pg))) + { + uniqueMap.remove(pg.getName()); + excluded.add(pg.getName()); + } + } + else + { + uniqueMap.put(pg.getName(), getBasePermissionGroup(pg)); + } + permissionReferenceMap.put(pg.toString(), pg); + permissionGroupMap.put(pg, pg); + } + for (Permission p : ps.getPermissions()) + { + if (uniqueMap.containsKey(p.getName()) && !excluded.contains(p.getName())) + { + PermissionReference value = uniqueMap.get(p.getName()); + if (!value.equals(p)) + { + uniqueMap.remove(p.getName()); + excluded.add(p.getName()); + } + } + else + { + uniqueMap.put(p.getName(), p); + } + permissionReferenceMap.put(p.toString(), p); + permissionMap.put(p, p); + } + } + // Add all permissions to the unique list + if (uniqueMap.containsKey(PermissionService.ALL_PERMISSIONS)) + { + throw new IllegalStateException( + "There must not be a permission with the same name as the ALL_PERMISSION constant: " + + PermissionService.ALL_PERMISSIONS); + } + uniqueMap.put(PermissionService.ALL_PERMISSIONS, new SimplePermissionReference(QName.createQName( + NamespaceService.SECURITY_MODEL_1_0_URI, PermissionService.ALL_PERMISSIONS), PermissionService.ALL_PERMISSIONS)); + + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelException.java b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelException.java new file mode 100644 index 0000000000..779fa531e1 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Exceptions related to the permissions model + * + * @author andyh + */ +public class PermissionModelException extends AlfrescoRuntimeException +{ + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = -5156253607792153538L; + + public PermissionModelException(String msg) + { + super(msg); + } + + public PermissionModelException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java new file mode 100644 index 0000000000..5b4da8992f --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.util.Set; + +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.AbstractPermissionTest; +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; +import org.alfresco.service.namespace.QName; + +public class PermissionModelTest extends AbstractPermissionTest +{ + + public PermissionModelTest() + { + super(); + } + + public void testIncludePermissionGroups() + { + Set grantees = permissionModelDAO.getGranteePermissions(new SimplePermissionReference(QName.createQName("cm", "folder", + namespacePrefixResolver), "Guest")); + + assertEquals(5, grantees.size()); + } + + public void testGetGrantingPermissions() + { + Set granters = permissionModelDAO.getGrantingPermissions(new SimplePermissionReference(QName.createQName("sys", "base", + namespacePrefixResolver), "ReadProperties")); + assertEquals(8, granters.size()); + } + + public void testGlobalPermissions() + { + Set globalPermissions = permissionModelDAO.getGlobalPermissionEntries(); + assertEquals(5, globalPermissions.size()); + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionSet.java b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionSet.java new file mode 100644 index 0000000000..c6054e19a0 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionSet.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.dom4j.Attribute; +import org.dom4j.Element; + +/** + * Store and read the definition of a permission set + * @author andyh + */ +public class PermissionSet implements XMLModelInitialisable +{ + private static final String TYPE = "type"; + private static final String PERMISSION_GROUP = "permissionGroup"; + private static final String PERMISSION = "permission"; + private static final String EXPOSE = "expose"; + private static final String EXPOSE_ALL = "all"; + //private static final String EXPOSE_SELECTED = "selected"; + + + private QName qname; + + private boolean exposeAll; + + private Set permissionGroups = new HashSet(); + + private Set permissions = new HashSet(); + + public PermissionSet() + { + super(); + } + + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel) + { + qname = QName.createQName(element.attributeValue(TYPE), nspr); + + Attribute exposeAttribute = element.attribute(EXPOSE); + if(exposeAttribute != null) + { + exposeAll = exposeAttribute.getStringValue().equalsIgnoreCase(EXPOSE_ALL); + } + else + { + exposeAll = true; + } + + for(Iterator pgit = element.elementIterator(PERMISSION_GROUP); pgit.hasNext(); /**/) + { + Element permissionGroupElement = (Element)pgit.next(); + PermissionGroup permissionGroup = new PermissionGroup(qname); + permissionGroup.initialise(permissionGroupElement, nspr, permissionModel); + permissionGroups.add(permissionGroup); + } + + for(Iterator pit = element.elementIterator(PERMISSION); pit.hasNext(); /**/) + { + Element permissionElement = (Element)pit.next(); + Permission permission = new Permission(qname); + permission.initialise(permissionElement, nspr, permissionModel); + permissions.add(permission); + } + + } + + public Set getPermissionGroups() + { + return Collections.unmodifiableSet(permissionGroups); + } + + public Set getPermissions() + { + return Collections.unmodifiableSet(permissions); + } + + public QName getQName() + { + return qname; + } + + public boolean exposeAll() + { + return exposeAll; + } + + + +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/XMLModelInitialisable.java b/source/java/org/alfresco/repo/security/permissions/impl/model/XMLModelInitialisable.java new file mode 100644 index 0000000000..c4ddd467d2 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/XMLModelInitialisable.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.impl.model; + +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.dom4j.Element; + +/** + * Interface to initialise a component of the permission mode from its XML representation. + * + * @author andyh + */ +public interface XMLModelInitialisable +{ + public void initialise(Element element, NamespacePrefixResolver nspr, PermissionModel permissionModel); +} diff --git a/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java b/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java new file mode 100644 index 0000000000..c81e0c6d45 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.permissions.noop; + +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.repo.security.permissions.NodePermissionEntry; +import org.alfresco.repo.security.permissions.PermissionEntry; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.PermissionServiceSPI; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.QName; + + +/** + * Dummy implementation of Permissions Service + * + */ +public class PermissionServiceNOOPImpl + implements PermissionServiceSPI +{ + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getOwnerAuthority() + */ + public String getOwnerAuthority() + { + return OWNER_AUTHORITY; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getAllAuthorities() + */ + public String getAllAuthorities() + { + return ALL_AUTHORITIES; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getAllPermission() + */ + public String getAllPermission() + { + return ALL_PERMISSIONS; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getPermissions(org.alfresco.service.cmr.repository.NodeRef) + */ + public Set getPermissions(NodeRef nodeRef) + { + return null; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getAllPermissions(org.alfresco.service.cmr.repository.NodeRef) + */ + public Set getAllSetPermissions(NodeRef nodeRef) + { + return null; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getSettablePermissions(org.alfresco.service.cmr.repository.NodeRef) + */ + public Set getSettablePermissions(NodeRef nodeRef) + { + return getSettablePermissions((QName)null); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#getSettablePermissions(org.alfresco.service.namespace.QName) + */ + public Set getSettablePermissions(QName type) + { + HashSet permissions = new HashSet(); + permissions.add(ALL_PERMISSIONS); + return permissions; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#hasPermission(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.repo.security.permissions.PermissionReference) + */ + public AccessStatus hasPermission(NodeRef nodeRef, String perm) + { + return AccessStatus.ALLOWED; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#deletePermissions(org.alfresco.service.cmr.repository.NodeRef) + */ + public void deletePermissions(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#deletePermission(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.repo.security.permissions.PermissionReference, boolean) + */ + public void deletePermission(NodeRef nodeRef, String authority, String perm, boolean allow) + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#setPermission(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.repo.security.permissions.PermissionReference, boolean) + */ + public void setPermission(NodeRef nodeRef, String authority, String perm, boolean allow) + { + } + + /* (non-Javadoc) + * @see org.alfresco.repo.security.permissions.PermissionService#setInheritParentPermissions(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + public void setInheritParentPermissions(NodeRef nodeRef, boolean inheritParentPermissions) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.security.PermissionService#getInheritParentPermissions(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean getInheritParentPermissions(NodeRef nodeRef) + { + // TODO Auto-generated method stub + return true; + } + + public void clearPermission(NodeRef nodeRef, String authority) + { + + } + + // SPI + + public void deletePermission(PermissionEntry permissionEntry) + { + + } + + public void deletePermissions(NodePermissionEntry nodePermissionEntry) + { + + } + + public void deletePermissions(String recipient) + { + + } + + public NodePermissionEntry explainPermission(NodeRef nodeRef, PermissionReference perm) + { + throw new UnsupportedOperationException(); + } + + public PermissionReference getAllPermissionReference() + { + throw new UnsupportedOperationException(); + } + + public String getPermission(PermissionReference permissionReference) + { + throw new UnsupportedOperationException(); + } + + public PermissionReference getPermissionReference(QName qname, String permissionName) + { + throw new UnsupportedOperationException(); + } + + public PermissionReference getPermissionReference(String permissionName) + { + throw new UnsupportedOperationException(); + } + + public NodePermissionEntry getSetPermissions(NodeRef nodeRef) + { + throw new UnsupportedOperationException(); + } + + public Set getSettablePermissionReferences(NodeRef nodeRef) + { + throw new UnsupportedOperationException(); + } + + public Set getSettablePermissionReferences(QName type) + { + throw new UnsupportedOperationException(); + } + + public AccessStatus hasPermission(NodeRef nodeRef, PermissionReference perm) + { + throw new UnsupportedOperationException(); + } + + public void setPermission(NodePermissionEntry nodePermissionEntry) + { + throw new UnsupportedOperationException(); + } + + public void setPermission(PermissionEntry permissionEntry) + { + throw new UnsupportedOperationException(); + } +} diff --git a/source/java/org/alfresco/repo/security/person/PersonException.java b/source/java/org/alfresco/repo/security/person/PersonException.java new file mode 100644 index 0000000000..3bbef6c8e1 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/PersonException.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.person; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * All exceptions related to the person service. + * + * @author Andy Hind + */ +public class PersonException extends AlfrescoRuntimeException +{ + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 2802163127696444600L; + + public PersonException(String msgId) + { + super(msgId); + } + + public PersonException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + public PersonException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + public PersonException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } + +} diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java new file mode 100644 index 0000000000..ba1458d979 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.person; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.QueryParameterDefImpl; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +public class PersonServiceImpl implements PersonService +{ + public static final String SYSTEM_FOLDER = "/sys:system"; + + public static final String PEOPLE_FOLDER = SYSTEM_FOLDER + "/sys:people"; + + // IOC + + private StoreRef storeRef; + + private NodeService nodeService; + + private DictionaryService dictionaryService; + + private SearchService searchService; + + private NamespacePrefixResolver namespacePrefixResolver; + + private boolean createMissingPeople; + + private boolean userNamesAreCaseSensitive; + + private String companyHomePath; + + private NodeRef companyHomeNodeRef; + + private static Set mutableProperties; + + static + { + Set props = new HashSet(); + props.add(ContentModel.PROP_HOMEFOLDER); + props.add(ContentModel.PROP_FIRSTNAME); + // Middle Name + props.add(ContentModel.PROP_LASTNAME); + props.add(ContentModel.PROP_EMAIL); + props.add(ContentModel.PROP_ORGID); + mutableProperties = Collections.unmodifiableSet(props); + } + + public PersonServiceImpl() + { + super(); + } + + public boolean getUserNamesAreCaseSensitive() + { + return userNamesAreCaseSensitive; + } + + public void setUserNamesAreCaseSensitive(boolean userNamesAreCaseSensitive) + { + this.userNamesAreCaseSensitive = userNamesAreCaseSensitive; + } + + public NodeRef getPerson(String caseSensitiveUserName) + { + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName : caseSensitiveUserName.toLowerCase(); + NodeRef personNode = getPersonOrNull(userName); + if (personNode == null) + { + if (createMissingPeople()) + { + return createMissingPerson(userName); + } + else + { + throw new PersonException("No person found for user name " + userName); + } + + } + else + { + return personNode; + } + } + + public boolean personExists(String caseSensitiveUserName) + { + return getPersonOrNull(caseSensitiveUserName) != null; + } + + public NodeRef getPersonOrNull(String caseSensitiveUserName) + { + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName : caseSensitiveUserName.toLowerCase(); + NodeRef rootNode = nodeService.getRootNode(storeRef); + QueryParameterDefinition[] defs = new QueryParameterDefinition[1]; + DataTypeDefinition text = dictionaryService.getDataType(DataTypeDefinition.TEXT); + defs[0] = new QueryParameterDefImpl(QName.createQName("cm", "var", namespacePrefixResolver), text, true, + userName); + List results = searchService.selectNodes(rootNode, PEOPLE_FOLDER + + "/cm:person[@cm:userName = $cm:var ]", defs, namespacePrefixResolver, false); + if (results.size() != 1) + { + return null; + } + return results.get(0); + } + + public boolean createMissingPeople() + { + return createMissingPeople; + } + + public Set getMutableProperties() + { + return mutableProperties; + } + + public void setPersonProperties(String caseSensitiveUserName, Map properties) + { + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName : caseSensitiveUserName.toLowerCase(); + NodeRef personNode = getPersonOrNull(userName); + if (personNode == null) + { + if (createMissingPeople()) + { + personNode = createMissingPerson(userName); + } + else + { + throw new PersonException("No person found for user name " + userName); + } + + } + + properties.put(ContentModel.PROP_USERNAME, userName); + + nodeService.setProperties(personNode, properties); + } + + public boolean isMutable() + { + return true; + } + + private NodeRef createMissingPerson(String userName) + { + HashMap properties = getDefaultProperties(userName); + return createPerson(properties); + } + + private HashMap getDefaultProperties(String userName) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + properties.put(ContentModel.PROP_HOMEFOLDER, getCompanyHome()); + properties.put(ContentModel.PROP_FIRSTNAME, userName); + properties.put(ContentModel.PROP_LASTNAME, ""); + properties.put(ContentModel.PROP_EMAIL, ""); + properties.put(ContentModel.PROP_ORGID, ""); + return properties; + } + + public NodeRef createPerson(Map properties) + { + String caseSensitiveUserName = DefaultTypeConverter.INSTANCE.convert(String.class, properties + .get(ContentModel.PROP_USERNAME)); + String userName = userNamesAreCaseSensitive ? caseSensitiveUserName : caseSensitiveUserName.toLowerCase(); + properties.put(ContentModel.PROP_USERNAME, userName); + return nodeService.createNode(getPeopleContainer(), ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_PERSON, + ContentModel.TYPE_PERSON, properties).getChildRef(); + } + + public NodeRef getPeopleContainer() + { + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + List results = searchService.selectNodes(rootNodeRef, PEOPLE_FOLDER, null, namespacePrefixResolver, + false); + NodeRef typesNode = null; + if (results.size() == 0) + { + + List result = nodeService.getChildAssocs(rootNodeRef, RegexQNamePattern.MATCH_ALL, + QName.createQName("sys", "system", namespacePrefixResolver)); + NodeRef sysNode = null; + if (result.size() == 0) + { + sysNode = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, + QName.createQName("sys", "system", namespacePrefixResolver), ContentModel.TYPE_CONTAINER) + .getChildRef(); + } + else + { + sysNode = result.get(0).getChildRef(); + } + result = nodeService.getChildAssocs(sysNode, RegexQNamePattern.MATCH_ALL, QName.createQName("sys", + "people", namespacePrefixResolver)); + + if (result.size() == 0) + { + typesNode = nodeService.createNode(sysNode, ContentModel.ASSOC_CHILDREN, + QName.createQName("sys", "people", namespacePrefixResolver), ContentModel.TYPE_CONTAINER) + .getChildRef(); + return typesNode; + } + else + { + return result.get(0).getChildRef(); + } + + } + else + { + return results.get(0); + } + } + + public void deletePerson(String userName) + { + NodeRef personNodeRef = getPersonOrNull(userName); + if (personNodeRef != null) + { + nodeService.deleteNode(personNodeRef); + } + + } + + public Set getAllPeople() + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + List results = searchService.selectNodes(rootNode, PEOPLE_FOLDER + "/cm:person", null, + namespacePrefixResolver, false); + HashSet all = new HashSet(); + all.addAll(results); + return all; + } + + public void setCreateMissingPeople(boolean createMissingPeople) + { + this.createMissingPeople = createMissingPeople; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setStoreUrl(String storeUrl) + { + this.storeRef = new StoreRef(storeUrl); + } + + public void setCompanyHomePath(String companyHomePath) + { + this.companyHomePath = companyHomePath; + } + + public synchronized NodeRef getCompanyHome() + { + if (companyHomeNodeRef == null) + { + List refs = searchService.selectNodes(nodeService.getRootNode(storeRef), companyHomePath, null, + namespacePrefixResolver, false); + if (refs.size() != 1) + { + throw new IllegalStateException("Invalid company home path: found : " + refs.size()); + } + companyHomeNodeRef = refs.get(0); + } + return companyHomeNodeRef; + } + + // IOC Setters + +} diff --git a/source/java/org/alfresco/repo/security/person/PersonTest.java b/source/java/org/alfresco/repo/security/person/PersonTest.java new file mode 100644 index 0000000000..c710d1864e --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/PersonTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.security.person; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; + +public class PersonTest extends BaseSpringTest +{ + + private PersonService personService; + + private NodeService nodeService; + + private NodeRef rootNodeRef; + + public PersonTest() + { + super(); + // TODO Auto-generated constructor stub + } + + protected void onSetUpInTransaction() throws Exception + { + personService = (PersonService) applicationContext.getBean("personService"); + nodeService = (NodeService) applicationContext.getBean("nodeService"); + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + for(NodeRef nodeRef: personService.getAllPeople()) + { + nodeService.deleteNode(nodeRef); + } + + } + + protected void onTearDownInTransaction() + { + super.onTearDownInTransaction(); + flushAndClear(); + } + + public void testCreateMissingPeople() + { + personService.setCreateMissingPeople(false); + assertFalse(personService.createMissingPeople()); + + personService.setCreateMissingPeople(true); + assertTrue(personService.createMissingPeople()); + + personService.setCreateMissingPeople(false); + try + { + personService.getPerson("andy"); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + + personService.setCreateMissingPeople(true); + NodeRef nodeRef = personService.getPerson("andy"); + assertNotNull(nodeRef); + testProperties(nodeRef, "andy", "andy", "", "", ""); + + personService.setCreateMissingPeople(false); + try + { + personService.setPersonProperties("derek", createDefaultProperties("derek", "Derek", "Hulley", "dh@dh", + "alfresco", rootNodeRef)); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + + personService.setCreateMissingPeople(true); + personService.setPersonProperties("derek", createDefaultProperties("derek", "Derek", "Hulley", "dh@dh", + "alfresco", rootNodeRef)); + testProperties(personService.getPerson("derek"), "derek", "Derek", "Hulley", "dh@dh", "alfresco"); + + testProperties(personService.getPerson("andy"), "andy", "andy", "", "", ""); + + assertEquals(2, personService.getAllPeople().size()); + assertTrue(personService.getAllPeople().contains(personService.getPerson("andy"))); + assertTrue(personService.getAllPeople().contains(personService.getPerson("derek"))); + + setComplete(); + endTransaction(); + } + + public void testMutableProperties() + { + assertEquals(5, personService.getMutableProperties().size()); + assertTrue(personService.getMutableProperties().contains(ContentModel.PROP_HOMEFOLDER)); + assertTrue(personService.getMutableProperties().contains(ContentModel.PROP_FIRSTNAME)); + assertTrue(personService.getMutableProperties().contains(ContentModel.PROP_LASTNAME)); + assertTrue(personService.getMutableProperties().contains(ContentModel.PROP_EMAIL)); + assertTrue(personService.getMutableProperties().contains(ContentModel.PROP_ORGID)); + + setComplete(); + endTransaction(); + } + + public void testPersonCRUD() + { + personService.setCreateMissingPeople(false); + try + { + personService.getPerson("derek"); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + personService.setCreateMissingPeople(false); + personService.createPerson(createDefaultProperties("derek", "Derek", "Hulley", "dh@dh", + "alfresco", rootNodeRef)); + testProperties(personService.getPerson("derek"), "derek", "Derek", "Hulley", "dh@dh", "alfresco"); + + personService.setPersonProperties("derek", createDefaultProperties("derek", "Derek_", "Hulley_", "dh@dh_", + "alfresco_", rootNodeRef)); + + testProperties(personService.getPerson("derek"), "derek", "Derek_", "Hulley_", "dh@dh_", "alfresco_"); + + personService.setPersonProperties("derek", createDefaultProperties("derek", "Derek", "Hulley", "dh@dh", + "alfresco", rootNodeRef)); + + testProperties(personService.getPerson("derek"), "derek", "Derek", "Hulley", "dh@dh", "alfresco"); + + assertEquals(1, personService.getAllPeople().size()); + assertTrue(personService.getAllPeople().contains(personService.getPerson("derek"))); + + personService.deletePerson("derek"); + assertEquals(0, personService.getAllPeople().size()); + try + { + personService.getPerson("derek"); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + + setComplete(); + endTransaction(); + } + + private void testProperties(NodeRef nodeRef, String userName, String firstName, String lastName, String email, + String orgId) + { + assertEquals(userName, DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, + ContentModel.PROP_USERNAME))); + assertNotNull(nodeService.getProperty(nodeRef, ContentModel.PROP_HOMEFOLDER)); + assertEquals(firstName, DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, + ContentModel.PROP_FIRSTNAME))); + assertEquals(lastName, DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, + ContentModel.PROP_LASTNAME))); + assertEquals(email, DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, + ContentModel.PROP_EMAIL))); + assertEquals(orgId, DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(nodeRef, + ContentModel.PROP_ORGID))); + } + + private Map createDefaultProperties(String userName, String firstName, String lastName, + String email, String orgId, NodeRef home) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + properties.put(ContentModel.PROP_HOMEFOLDER, home); + properties.put(ContentModel.PROP_FIRSTNAME, firstName); + properties.put(ContentModel.PROP_LASTNAME, lastName); + properties.put(ContentModel.PROP_EMAIL, email); + properties.put(ContentModel.PROP_ORGID, orgId); + return properties; + } + + public void testCaseSensitive() + { + if(personService.getUserNamesAreCaseSensitive()) + { + personService.createPerson(createDefaultProperties("Derek", "Derek", "Hulley", "dh@dh", + "alfresco", rootNodeRef)); + + try + { + personService.getPerson("derek"); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + try + { + personService.getPerson("deRek"); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + try + { + personService.getPerson("DEREK"); + assertNotNull(null); + } + catch (PersonException pe) + { + + } + personService.getPerson("Derek"); + } + } + + public void testCaseInsensitive() + { + if(!personService.getUserNamesAreCaseSensitive()) + { + personService.createPerson(createDefaultProperties("Derek", "Derek", "Hulley", "dh@dh", + "alfresco", rootNodeRef)); + + personService.getPerson("derek"); + personService.getPerson("deRek"); + personService.getPerson("Derek"); + personService.getPerson("DEREK"); + } + } +} diff --git a/source/java/org/alfresco/repo/service/BeanServiceDescriptor.java b/source/java/org/alfresco/repo/service/BeanServiceDescriptor.java new file mode 100644 index 0000000000..54f80e6562 --- /dev/null +++ b/source/java/org/alfresco/repo/service/BeanServiceDescriptor.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import java.util.Collection; + +import org.alfresco.service.ServiceDescriptor; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + + +/** + * Service Descriptor. + * + * @author David Caruana + */ +public class BeanServiceDescriptor + implements ServiceDescriptor +{ + // Service Name + private QName serviceName; + + // Service Description + private String description; + + // Service interface class + private Class interfaceClass; + + // Supported Store Protocols + Collection protocols = null; + + // Supported Stores + Collection stores = null; + + + /*package*/ BeanServiceDescriptor(QName serviceName, ServiceDescriptorMetaData metaData, StoreRedirector redirector) + { + this.serviceName = serviceName; + this.interfaceClass = metaData.getInterface(); + this.description = metaData.getDescription(); + + if (redirector != null) + { + protocols = redirector.getSupportedStoreProtocols(); + stores = redirector.getSupportedStores(); + } + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceDescriptor#getQualifiedName() + */ + public QName getQualifiedName() + { + return serviceName; + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceDescriptor#getDescription() + */ + public String getDescription() + { + return description; + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceDescriptor#getInterface() + */ + public Class getInterface() + { + return interfaceClass; + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceDescriptor#getSupportedStoreProtocols() + */ + public Collection getSupportedStoreProtocols() + { + return protocols; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.StoreRedirector#getSupportedStores() + */ + public Collection getSupportedStores() + { + return stores; + } + +} diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorAdvisor.java b/source/java/org/alfresco/repo/service/ServiceDescriptorAdvisor.java new file mode 100644 index 0000000000..7ac1a984be --- /dev/null +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorAdvisor.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import org.springframework.aop.support.DefaultIntroductionAdvisor; + +/** + * Service Descriptor Advisor + * + * @author David Caruana + */ +public class ServiceDescriptorAdvisor extends DefaultIntroductionAdvisor +{ + private static final long serialVersionUID = -3327182176681357761L; + + + /** + * Construct Service Descriptor Advisor + * + * @param namespace service name namespace + * @param description service description + * @param interfaceClass service interface class + */ + public ServiceDescriptorAdvisor(String namespace, String description, Class interfaceClass) + { + super(new ServiceDescriptorMixin(namespace, description, interfaceClass), ServiceDescriptorMetaData.class); + } + +} diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorAdvisorFactory.java b/source/java/org/alfresco/repo/service/ServiceDescriptorAdvisorFactory.java new file mode 100644 index 0000000000..8df68f3bf8 --- /dev/null +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorAdvisorFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import org.springframework.beans.factory.FactoryBean; + +/** + * Factory for creating Service Descriptor Advisors. + * + * @author David Caruana + */ +public class ServiceDescriptorAdvisorFactory implements FactoryBean +{ + + private String namespace; + private String description; + private Class interfaceClass; + + + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + public Object getObject() throws Exception + { + return new ServiceDescriptorAdvisor(namespace, description, interfaceClass); + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + public Class getObjectType() + { + // TODO Auto-generated method stub + return ServiceDescriptorAdvisor.class; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#isSingleton() + */ + public boolean isSingleton() + { + // TODO Auto-generated method stub + return false; + } + + /** + * @param namespace the service name namespace + */ + public void setNamespace(String namespace) + { + this.namespace = namespace; + } + + /** + * @param description the service description + */ + public void setDescription(String description) + { + this.description = description; + } + + /** + * @param interfaceClass the service interface class + */ + public void setInterface(Class interfaceClass) + { + this.interfaceClass = interfaceClass; + } + +} diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorMetaData.java b/source/java/org/alfresco/repo/service/ServiceDescriptorMetaData.java new file mode 100644 index 0000000000..e30fe0f799 --- /dev/null +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorMetaData.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + + +/** + * Service Meta Data + * + * @author David Caruana + */ +public interface ServiceDescriptorMetaData +{ + /** + * @return the service name namespace + */ + public String getNamespace(); + + /** + * @return the service description + */ + public String getDescription(); + + /** + * @return the service interface class + */ + public Class getInterface(); + +} diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorMixin.java b/source/java/org/alfresco/repo/service/ServiceDescriptorMixin.java new file mode 100644 index 0000000000..deb8d67fe6 --- /dev/null +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorMixin.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import org.springframework.aop.support.DelegatingIntroductionInterceptor; + +/** + * Service Descriptor Mixin. + * + * @author David Caruana + */ +public class ServiceDescriptorMixin extends DelegatingIntroductionInterceptor + implements ServiceDescriptorMetaData +{ + private static final long serialVersionUID = -6511459263796802334L; + + private String namespace; + private String description; + private Class interfaceClass; + + + /** + * Construct Service Descriptor Mixin + * + * @param namespace + * @param description + * @param interfaceClass + */ + public ServiceDescriptorMixin(String namespace, String description, Class interfaceClass) + { + this.namespace = namespace; + this.description = description; + this.interfaceClass = interfaceClass; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceDescriptorMetaData#getNamespace() + */ + public String getNamespace() + { + return namespace; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceDescriptorMetaData#getDescription() + */ + public String getDescription() + { + return description; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceDescriptorMetaData#getInterface() + */ + public Class getInterface() + { + return interfaceClass; + } + +} diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java new file mode 100644 index 0000000000..a4cbfee87f --- /dev/null +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.service.ServiceDescriptor; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + + +/** + * Implementation of a Service Registry based on the definition of + * Services contained within a Spring Bean Factory. + * + * @author David Caruana + */ +public class ServiceDescriptorRegistry + implements BeanFactoryAware, BeanFactoryPostProcessor, ServiceRegistry +{ + // Bean Factory within which the registry lives + private BeanFactory beanFactory = null; + + // Service Descriptor map + private Map descriptors = new HashMap(); + + + /* (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory) + */ + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + Map beans = beanFactory.getBeansOfType(ServiceDescriptorMetaData.class); + Iterator iter = beans.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = (Map.Entry)iter.next(); + ServiceDescriptorMetaData metaData = (ServiceDescriptorMetaData)entry.getValue(); + QName serviceName = QName.createQName(metaData.getNamespace(), (String)entry.getKey()); + StoreRedirector redirector = (entry.getValue() instanceof StoreRedirector) ? (StoreRedirector)entry.getValue() : null; + BeanServiceDescriptor serviceDescriptor = new BeanServiceDescriptor(serviceName, metaData, redirector); + descriptors.put(serviceDescriptor.getQualifiedName(), serviceDescriptor); + } + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + public void setBeanFactory(BeanFactory beanFactory) throws BeansException + { + this.beanFactory = beanFactory; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getServices() + */ + public Collection getServices() + { + return Collections.unmodifiableSet(descriptors.keySet()); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#isServiceProvided(org.alfresco.repo.ref.QName) + */ + public boolean isServiceProvided(QName service) + { + return descriptors.containsKey(service); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getServiceDescriptor(org.alfresco.repo.ref.QName) + */ + public ServiceDescriptor getServiceDescriptor(QName service) + { + return descriptors.get(service); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getService(org.alfresco.repo.ref.QName) + */ + public Object getService(QName service) + { + return beanFactory.getBean(service.getLocalName()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getDescriptorService() + */ + public DescriptorService getDescriptorService() + { + return (DescriptorService)getService(DESCRIPTOR_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getNodeService() + */ + public NodeService getNodeService() + { + return (NodeService)getService(NODE_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getNodeService() + */ + public AuthenticationService getAuthenticationService() + { + return (AuthenticationService)getService(AUTHENTICATION_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getContentService() + */ + public ContentService getContentService() + { + return (ContentService)getService(CONTENT_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getMimetypeService() + */ + public MimetypeService getMimetypeService() + { + return (MimetypeService)getService(MIMETYPE_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getVersionService() + */ + public VersionService getVersionService() + { + return (VersionService)getService(VERSION_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getLockService() + */ + public LockService getLockService() + { + return (LockService)getService(LOCK_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.service.ServiceRegistry#getDictionaryService() + */ + public DictionaryService getDictionaryService() + { + return (DictionaryService)getService(DICTIONARY_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getSearchService() + */ + public SearchService getSearchService() + { + return (SearchService)getService(SEARCH_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getTransactionService() + */ + public TransactionService getTransactionService() + { + return (TransactionService)getService(TRANSACTION_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getCopyService() + */ + public CopyService getCopyService() + { + return (CopyService)getService(COPY_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getCheckOutCheckInService() + */ + public CheckOutCheckInService getCheckOutCheckInService() + { + return (CheckOutCheckInService)getService(COCI_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getCategoryService() + */ + public CategoryService getCategoryService() + { + return (CategoryService)getService(CATEGORY_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getNamespaceService() + */ + public NamespaceService getNamespaceService() + { + return (NamespaceService)getService(NAMESPACE_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getImporterService() + */ + public ImporterService getImporterService() + { + return (ImporterService)getService(IMPORTER_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getExporterService() + */ + public ExporterService getExporterService() + { + return (ExporterService)getService(EXPORTER_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getRuleService() + */ + public RuleService getRuleService() + { + return (RuleService)getService(RULE_SERVICE); + } + + /* + * (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getActionService() + */ + public ActionService getActionService() + { + return (ActionService)getService(ACTION_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getPermissionService() + */ + public PermissionService getPermissionService() + { + return (PermissionService)getService(PERMISSIONS_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getAuthorityService() + */ + public AuthorityService getAuthorityService() + { + return (AuthorityService)getService(AUTHORITY_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getTemplateService() + */ + public TemplateService getTemplateService() + { + return (TemplateService)getService(TEMPLATE_SERVICE); + } + + /* (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getTemplateService() + */ + public FileFolderService getFileFolderService() + { + return (FileFolderService) getService(FILE_FOLDER_SERVICE); + } +} diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorRegistryTest.java b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistryTest.java new file mode 100644 index 0000000000..207343d91c --- /dev/null +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistryTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import java.util.Collection; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.service.ServiceDescriptor; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class ServiceDescriptorRegistryTest extends TestCase +{ + + private ApplicationContext factory = null; + + private static String TEST_NAMESPACE = "http://www.alfresco.org/test/serviceregistrytest"; + private static QName invalidService = QName.createQName(TEST_NAMESPACE, "invalid"); + private static QName service1 = QName.createQName(TEST_NAMESPACE, "service1"); + private static QName service2 = QName.createQName(TEST_NAMESPACE, "service2"); + private static QName service3 = QName.createQName(TEST_NAMESPACE, "service3"); + + + public void setUp() + { + factory = new ClassPathXmlApplicationContext("org/alfresco/repo/service/testregistry.xml"); + } + + public void testDescriptor() + { + ServiceRegistry registry = (ServiceRegistry)factory.getBean("serviceRegistry"); + + Collection services = registry.getServices(); + assertNotNull(services); + assertEquals(3, services.size()); + + assertTrue(registry.isServiceProvided(service1)); + assertFalse(registry.isServiceProvided(invalidService)); + + ServiceDescriptor invalid = registry.getServiceDescriptor(invalidService); + assertNull(invalid); + ServiceDescriptor desc1 = registry.getServiceDescriptor(service1); + assertNotNull(desc1); + assertEquals(service1, desc1.getQualifiedName()); + assertEquals("Test Service 1", desc1.getDescription()); + assertEquals(TestServiceInterface.class, desc1.getInterface()); + ServiceDescriptor desc2 = registry.getServiceDescriptor(service2); + assertNotNull(desc2); + assertEquals(service2, desc2.getQualifiedName()); + assertEquals("Test Service 2", desc2.getDescription()); + assertEquals(TestServiceInterface.class, desc2.getInterface()); + } + + + public void testService() + { + ServiceRegistry registry = (ServiceRegistry)factory.getBean("serviceRegistry"); + + TestServiceInterface theService1 = (TestServiceInterface)registry.getService(service1); + assertNotNull(service1); + assertEquals("Test1:service1", theService1.test("service1")); + TestServiceInterface theService2 = (TestServiceInterface)registry.getService(service2); + assertNotNull(service2); + assertEquals("Test2:service2", theService2.test("service2")); + } + + + public void testStores() + { + ServiceRegistry registry = (ServiceRegistry)factory.getBean("serviceRegistry"); + + ServiceDescriptor desc3 = registry.getServiceDescriptor(service3); + assertNotNull(desc3); + StoreRedirector theService3 = (StoreRedirector)registry.getService(service3); + assertNotNull(service3); + + Collection descStores = desc3.getSupportedStoreProtocols(); + assertTrue(descStores.contains("Type1")); + assertTrue(descStores.contains("Type2")); + assertFalse(descStores.contains("Invalid")); + + Collection serviceStores = theService3.getSupportedStoreProtocols(); + for (String store: descStores) + { + assertTrue(serviceStores.contains(store)); + } + } + + + public void testAppContext() + { + ApplicationContext appContext = new ClassPathXmlApplicationContext("alfresco/application-context.xml"); + + ServiceRegistry registry = (ServiceRegistry)appContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + assertNotNull(registry); + NodeService s1 = registry.getNodeService(); + assertNotNull(s1); + CheckOutCheckInService s2 = registry.getCheckOutCheckInService(); + assertNotNull(s2); + ContentService s3 = registry.getContentService(); + assertNotNull(s3); + CopyService s4 = registry.getCopyService(); + assertNotNull(s4); + DictionaryService s5 = registry.getDictionaryService(); + assertNotNull(s5); + LockService s6 = registry.getLockService(); + assertNotNull(s6); + MimetypeService s7 = registry.getMimetypeService(); + assertNotNull(s7); + SearchService s8 = registry.getSearchService(); + assertNotNull(s8); + TransactionService transactionService = registry.getTransactionService(); + UserTransaction s9 = transactionService.getUserTransaction(); + assertNotNull(s9); + UserTransaction s10 = transactionService.getUserTransaction(); + assertNotNull(s10); + assertFalse(s9.equals(s10)); + VersionService s11 = registry.getVersionService(); + assertNotNull(s11); + } + + + public interface TestServiceInterface + { + public String test(String arg); + } + + public static abstract class Component implements TestServiceInterface + { + private String type; + + private Component(String type) + { + this.type = type; + } + + public String test(String arg) + { + return type + ":" + arg; + } + } + + public static class Test1Component extends Component + { + private Test1Component() + { + super("Test1"); + } + } + + public static class Test2Component extends Component + { + private Test2Component() + { + super("Test2"); + } + } + +} diff --git a/source/java/org/alfresco/repo/service/StoreRedirector.java b/source/java/org/alfresco/repo/service/StoreRedirector.java new file mode 100644 index 0000000000..d8a4df0c30 --- /dev/null +++ b/source/java/org/alfresco/repo/service/StoreRedirector.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import java.util.Collection; + +import org.alfresco.service.cmr.repository.StoreRef; + +public interface StoreRedirector +{ + /** + * @return the names of the protocols supported + */ + public Collection getSupportedStoreProtocols(); + + /** + * @return the Store Refs of the stores supported + */ + public Collection getSupportedStores(); +} diff --git a/source/java/org/alfresco/repo/service/StoreRedirectorProxyFactory.java b/source/java/org/alfresco/repo/service/StoreRedirectorProxyFactory.java new file mode 100644 index 0000000000..12b10ba820 --- /dev/null +++ b/source/java/org/alfresco/repo/service/StoreRedirectorProxyFactory.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.ServiceException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * This factory provides component redirection based on Store or Node References + * passed into the component. + * + * Redirection is driven by StoreRef and NodeRef parameters. If none are given + * in the method call, the default component is called. Otherwise, the store + * type is extracted from these parameters and the appropriate component called + * for the store type. + * + * An error is thrown if multiple store types are found. + * + * @author David Caruana + * + * @param The component interface class + */ +public class StoreRedirectorProxyFactory implements FactoryBean, InitializingBean +{ + // Logger + private static final Log logger = LogFactory.getLog(StoreRedirectorProxyFactory.class); + + // The component interface class + private Class proxyInterface = null; + + // The default component binding + private I defaultBinding = null; + + // The map of store types to component bindings + private Map redirectedProtocolBindings = null; + + // the map if more specific store Refs to component bindings + private Map redirectedStoreBindings = null; + + // The proxy responsible for redirection based on store type + private I redirectorProxy = null; + + /** + * Sets the proxy interface + * + * @param proxyInterface + * the proxy interface + */ + public void setProxyInterface(Class proxyInterface) + { + this.proxyInterface = proxyInterface; + } + + /** + * Sets the default component binding + * + * @param binding + * the component to call by default + */ + public void setDefaultBinding(I defaultBinding) + { + this.defaultBinding = defaultBinding; + } + + /** + * Sets the binding of store type (protocol string) to component + * + * @param bindings + * the bindings + */ + public void setRedirectedProtocolBindings(Map protocolBindings) + { + this.redirectedProtocolBindings = protocolBindings; + } + + /** + * Sets the binding of store type (protocol string) to component + * + * @param bindings + * the bindings + */ + public void setRedirectedStoreBindings(Map storeBindings) + { + redirectedStoreBindings = new HashMap(storeBindings.size()); + for(String ref : storeBindings.keySet()) + { + redirectedStoreBindings.put(new StoreRef(ref), storeBindings.get(ref)); + } + } + + + /* (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws ServiceException + { + ParameterCheck.mandatory("Proxy Interface", proxyInterface); + ParameterCheck.mandatory("Default Binding", defaultBinding); + + // Setup the redirector proxy + this.redirectorProxy = (I)Proxy.newProxyInstance(proxyInterface.getClassLoader(), new Class[] { proxyInterface, StoreRedirector.class }, new RedirectorInvocationHandler()); + } + + + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + public I getObject() + { + return redirectorProxy; + } + + + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + public Class getObjectType() + { + return proxyInterface; + } + + + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#isSingleton() + */ + public boolean isSingleton() + { + return true; + } + + + /** + * Invocation handler that redirects based on store type + */ + /* package */class RedirectorInvocationHandler implements InvocationHandler, StoreRedirector + { + + /* (non-Javadoc) + * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) + */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + // Handle StoreRedirector Interface + if (method.getDeclaringClass().equals(StoreRedirector.class)) + { + return method.invoke(this, args); + } + + // Otherwise, determine the apropriate implementation to invoke for + // the service interface method + Object binding = null; + StoreRef storeRef = getStoreRef(args); + if (storeRef == null) + { + binding = StoreRedirectorProxyFactory.this.defaultBinding; + } + else + { + if (StoreRedirectorProxyFactory.this.redirectedStoreBindings != null) + { + binding = StoreRedirectorProxyFactory.this.redirectedStoreBindings.get(storeRef); + } + if ((binding == null) && (StoreRedirectorProxyFactory.this.redirectedProtocolBindings != null)) + { + binding = StoreRedirectorProxyFactory.this.redirectedProtocolBindings.get(storeRef.getProtocol()); + } + if (binding == null) + { + binding = StoreRedirectorProxyFactory.this.defaultBinding; + } + if (binding == null) + { + throw new ServiceException("Store type " + storeRef + " is not supported"); + } + } + + if (logger.isDebugEnabled()) + logger.debug("Redirecting method " + method + " based on store type " + storeRef); + + try + { + // Invoke the appropriate binding + return method.invoke(binding, args); + } + catch (InvocationTargetException e) + { + throw e.getCause(); + } + } + + + /** + * Determine store type from array of method arguments + * + * @param args the method arguments + * @return the store type (or null, if one is not specified) + */ + private StoreRef getStoreRef(Object[] args) + { + StoreRef storeRef = null; + + if(args == null) + { + return null; + } + + for (Object arg : args) + { + // Extract store type from argument, if store type provided + StoreRef argStoreRef = null; + if (arg instanceof NodeRef) + { + argStoreRef = ((NodeRef) arg).getStoreRef(); + } + else if (arg instanceof StoreRef) + { + argStoreRef = ((StoreRef) arg); + } + + // Only allow one store type + if (argStoreRef != null) + { + if (storeRef != null && !storeRef.equals(argStoreRef)) + { + throw new ServiceException("Multiple store types are not supported - types " + storeRef + " and " + argStoreRef + " passed"); + } + storeRef = argStoreRef; + } + } + + return storeRef; + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.service.StoreRedirector#getSupportedStoreProtocols() + */ + public Collection getSupportedStoreProtocols() + { + return Collections.unmodifiableCollection(StoreRedirectorProxyFactory.this.redirectedProtocolBindings.keySet()); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.service.StoreRedirector#getSupportedStores() + */ + public Collection getSupportedStores() + { + return Collections.unmodifiableCollection(StoreRedirectorProxyFactory.this.redirectedStoreBindings.keySet()); + } + + } +} diff --git a/source/java/org/alfresco/repo/service/StoreRedirectorProxyFactoryTest.java b/source/java/org/alfresco/repo/service/StoreRedirectorProxyFactoryTest.java new file mode 100644 index 0000000000..490991d7f0 --- /dev/null +++ b/source/java/org/alfresco/repo/service/StoreRedirectorProxyFactoryTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.service; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import junit.framework.TestCase; + +public class StoreRedirectorProxyFactoryTest extends TestCase +{ + + private ApplicationContext factory = null; + + public void setUp() + { + factory = new ClassPathXmlApplicationContext("org/alfresco/repo/service/testredirector.xml"); + } + + public void testRedirect() + { + StoreRef storeRef1 = new StoreRef("Type1", "id"); + StoreRef storeRef2 = new StoreRef("Type2", "id"); + StoreRef storeRef3 = new StoreRef("Type3", "id"); + StoreRef storeRef4 = new StoreRef("Type3", "woof"); + NodeRef nodeRef1 = new NodeRef(storeRef1, "id"); + NodeRef nodeRef2 = new NodeRef(storeRef2, "id"); + + TestServiceInterface service = (TestServiceInterface) factory.getBean("redirector_service1"); + + String result1 = service.defaultBinding("redirector_service1"); + assertEquals("Type1:redirector_service1", result1); + String result1a = service.noArgs(); + assertEquals("Type1", result1a); + String result2 = service.storeRef(storeRef1); + assertEquals("Type1:" + storeRef1, result2); + String result3 = service.storeRef(storeRef2); + assertEquals("Type2:" + storeRef2, result3); + String result4 = service.nodeRef(nodeRef1); + assertEquals("Type1:" + nodeRef1, result4); + String result5 = service.nodeRef(nodeRef2); + assertEquals("Type2:" + nodeRef2, result5); + String result6 = service.multiStoreRef(storeRef1, storeRef1); + assertEquals("Type1:" + storeRef1 + "," + storeRef1, result6); + String result7 = service.multiStoreRef(storeRef2, storeRef2); + assertEquals("Type2:" + storeRef2 + "," + storeRef2, result7); + String result8 = service.multiNodeRef(nodeRef1, nodeRef1); + assertEquals("Type1:" + nodeRef1 + "," + nodeRef1, result8); + String result9 = service.multiNodeRef(nodeRef2, nodeRef2); + assertEquals("Type2:" + nodeRef2 + "," + nodeRef2, result9); + String result10 = service.mixedStoreNodeRef(storeRef1, nodeRef1); + assertEquals("Type1:" + storeRef1 + "," + nodeRef1, result10); + String result11 = service.mixedStoreNodeRef(storeRef2, nodeRef2); + assertEquals("Type2:" + storeRef2 + "," + nodeRef2, result11); + String result12 = service.mixedStoreNodeRef(null, null); + assertEquals("Type1:null,null", result12); + String result13 = service.mixedStoreNodeRef(storeRef1, null); + assertEquals("Type1:" + storeRef1 + ",null", result13); + + // Direct store refs + String result14 = service.storeRef(storeRef3); + assertEquals("Type3:" + storeRef3, result14); + String result15 = service.storeRef(storeRef4); + assertEquals("Type1:" + storeRef4, result15); + } + + public void testInvalidArgs() + { + StoreRef defaultRef = new StoreRef("Type1", "id"); + StoreRef storeRef1 = new StoreRef("InvalidType1", "id"); + NodeRef nodeRef1 = new NodeRef(storeRef1, "id"); + + TestServiceInterface service = (TestServiceInterface) factory.getBean("redirector_service1"); + String result1 = service.storeRef(storeRef1); + assertEquals("Type1:" + storeRef1, result1); + String result2 = service.nodeRef(nodeRef1); + assertEquals("Type1:" + nodeRef1, result2); + } + + public void testException() + { + StoreRef storeRef1 = new StoreRef("Type1", "id"); + NodeRef nodeRef1 = new NodeRef(storeRef1, "id"); + TestServiceInterface service = (TestServiceInterface) factory.getBean("redirector_service1"); + + try + { + service.throwException(nodeRef1); + fail("Service method did not throw exception"); + } + catch(Exception e) + { + assertTrue(e instanceof IllegalArgumentException); + assertEquals(nodeRef1.toString(), e.getMessage()); + } + } + + + public interface TestServiceInterface + { + public String noArgs(); + + public String defaultBinding(String arg); + + public String storeRef(StoreRef ref1); + + public String nodeRef(NodeRef ref1); + + public String multiStoreRef(StoreRef ref1, StoreRef ref2); + + public String multiNodeRef(NodeRef ref1, NodeRef ref2); + + public String mixedStoreNodeRef(StoreRef ref2, NodeRef ref1); + + public void throwException(NodeRef ref1); + } + + + public static abstract class Component implements TestServiceInterface + { + private String type; + + private Component(String type) + { + this.type = type; + } + + public String noArgs() + { + return type; + } + + public String defaultBinding(String arg) + { + return type + ":" + arg; + } + + public String nodeRef(NodeRef ref1) + { + return type + ":" + ref1; + } + + public String storeRef(StoreRef ref1) + { + return type + ":" + ref1; + } + + public String multiNodeRef(NodeRef ref1, NodeRef ref2) + { + return type + ":" + ref1 + "," + ref2; + } + + public String multiStoreRef(StoreRef ref1, StoreRef ref2) + { + return type + ":" + ref1 + "," + ref2; + } + + public String mixedStoreNodeRef(StoreRef ref1, NodeRef ref2) + { + return type + ":" + ref1 + "," + ref2; + } + + public void throwException(NodeRef ref1) + { + throw new IllegalArgumentException(ref1.toString()); + } + + } + + public static class Type1Component extends Component + { + private Type1Component() + { + super("Type1"); + } + } + + public static class Type2Component extends Component + { + private Type2Component() + { + super("Type2"); + } + } + + public static class Type3Component extends Component + { + private Type3Component() + { + super("Type3"); + } + } + +} diff --git a/source/java/org/alfresco/repo/service/serviceregistrytest_model.xml b/source/java/org/alfresco/repo/service/serviceregistrytest_model.xml new file mode 100644 index 0000000000..2abf7535d6 --- /dev/null +++ b/source/java/org/alfresco/repo/service/serviceregistrytest_model.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/source/java/org/alfresco/repo/service/testredirector.xml b/source/java/org/alfresco/repo/service/testredirector.xml new file mode 100644 index 0000000000..3009eb68af --- /dev/null +++ b/source/java/org/alfresco/repo/service/testredirector.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + org.alfresco.repo.service.StoreRedirectorProxyFactoryTest$TestServiceInterface + + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/service/testregistry.xml b/source/java/org/alfresco/repo/service/testregistry.xml new file mode 100644 index 0000000000..56ec5740ac --- /dev/null +++ b/source/java/org/alfresco/repo/service/testregistry.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + alfresco/model/dictionaryModel.xml + + + + + + + + + http://www.alfresco.org/test/serviceregistrytest + + + + + + + org.alfresco.repo.service.ServiceDescriptorRegistryTest$TestServiceInterface + + + + + + + + + + + + + + org.alfresco.repo.service.ServiceDescriptorRegistryTest$TestServiceInterface + + + Test Service 1 + + + + + + + org.alfresco.repo.service.ServiceDescriptorRegistryTest$TestServiceInterface + + + + + + + + + + + + + + org.alfresco.repo.service.ServiceDescriptorRegistryTest$TestServiceInterface + + + Test Service 2 + + + + + + org.alfresco.repo.service.ServiceDescriptorRegistryTest$TestServiceInterface, org.alfresco.repo.service.StoreRedirector + + + + + + + + + + + + + + org.alfresco.repo.service.ServiceDescriptorRegistryTest$TestServiceInterface + + + Test Service 3 + + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/template/BasePathResultsMap.java b/source/java/org/alfresco/repo/template/BasePathResultsMap.java new file mode 100644 index 0000000000..31d0f3dfc7 --- /dev/null +++ b/source/java/org/alfresco/repo/template/BasePathResultsMap.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.TemplateNode; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A special Map that executes an XPath against the parent Node as part of the get() + * Map interface implementation. + * + * @author Kevin Roast + */ +public abstract class BasePathResultsMap extends HashMap implements Cloneable +{ + protected static Log logger = LogFactory.getLog(BasePathResultsMap.class); + protected TemplateNode parent; + protected ServiceRegistry services = null; + + /** + * Constructor + * + * @param parent The parent TemplateNode to execute searches from + * @param services The ServiceRegistry to use + */ + public BasePathResultsMap(TemplateNode parent, ServiceRegistry services) + { + super(1, 1.0f); + this.services = services; + this.parent = parent; + } + + /** + * @see java.util.Map#get(java.lang.Object) + */ + public abstract Object get(Object key); + + protected List getChildrenByXPath(String xpath, boolean firstOnly) + { + List result = null; + + if (xpath.length() != 0) + { + if (logger.isDebugEnabled()) + logger.debug("Executing xpath: " + xpath); + + List nodes = this.services.getSearchService().selectNodes( + this.parent.getNodeRef(), + xpath, + null, + this.services.getNamespaceService(), + false); + + // see if we only want the first result + if (firstOnly == true) + { + if (nodes.size() != 0) + { + result = new ArrayList(1); + result.add(new TemplateNode(nodes.get(0), this.services, this.parent.getImageResolver())); + } + } + // or all the results + else + { + result = new ArrayList(nodes.size()); + for (NodeRef ref : nodes) + { + result.add(new TemplateNode(ref, this.services, this.parent.getImageResolver())); + } + } + } + + return result != null ? result : new ArrayList(0); + } +} diff --git a/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java b/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java new file mode 100644 index 0000000000..fd8e09a1b9 --- /dev/null +++ b/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.ApplicationContextHelper; + +import freemarker.cache.TemplateLoader; + +/** + * Custom FreeMarker template loader to locate templates stored either from the ClassPath + * or in a Alfresco Repository. + *

    + * The template name should be supplied either as a NodeRef String or a ClassPath path String. + * + * @author Kevin Roast + */ +public class ClassPathRepoTemplateLoader implements TemplateLoader +{ + private NodeService nodeService; + private ContentService contentService; + + public ClassPathRepoTemplateLoader(NodeService nodeService, ContentService contentService) + { + if (nodeService == null) + { + throw new IllegalArgumentException("NodeService is mandatory."); + } + if (contentService == null) + { + throw new IllegalArgumentException("ContentService is mandatory."); + } + this.nodeService = nodeService; + this.contentService = contentService; + } + + /** + * Return an object wrapping a source for a template + */ + public Object findTemplateSource(String name) + throws IOException + { + if (name.indexOf(StoreRef.URI_FILLER) != -1) + { + NodeRef ref = new NodeRef(name); + if (this.nodeService.exists(ref) == true) + { + return new RepoTemplateSource(ref); + } + else + { + return null; + } + } + else + { + URL url = this.getClass().getClassLoader().getResource(name); + return url == null ? null : new ClassPathTemplateSource(url); + } + } + + public long getLastModified(Object templateSource) + { + return ((BaseTemplateSource)templateSource).lastModified(); + } + + public Reader getReader(Object templateSource, String encoding) throws IOException + { + return ((BaseTemplateSource)templateSource).getReader(); + } + + public void closeTemplateSource(Object templateSource) throws IOException + { + ((BaseTemplateSource)templateSource).close(); + } + + + /** + * Class used as a base for custom Template Source objects + */ + abstract class BaseTemplateSource + { + public abstract Reader getReader() throws IOException; + + public abstract void close() throws IOException; + + public abstract long lastModified(); + } + + + /** + * Class providing a ClassPath based Template Source + */ + class ClassPathTemplateSource extends BaseTemplateSource + { + private final URL url; + private URLConnection conn; + private InputStream inputStream; + + ClassPathTemplateSource(URL url) throws IOException + { + this.url = url; + this.conn = url.openConnection(); + } + + public boolean equals(Object o) + { + if (o instanceof ClassPathTemplateSource) + { + return url.equals(((ClassPathTemplateSource)o).url); + } + else + { + return false; + } + } + + public int hashCode() + { + return url.hashCode(); + } + + public String toString() + { + return url.toString(); + } + + public long lastModified() + { + return conn.getLastModified(); + } + + public Reader getReader() throws IOException + { + inputStream = conn.getInputStream(); + return new InputStreamReader(inputStream); + } + + public void close() throws IOException + { + try + { + if (inputStream != null) + { + inputStream.close(); + } + } + finally + { + inputStream = null; + conn = null; + } + } + } + + /** + * Class providing a Repository based Template Source + */ + class RepoTemplateSource extends BaseTemplateSource + { + private final NodeRef nodeRef; + private InputStream inputStream; + private ContentReader conn; + + RepoTemplateSource(NodeRef ref) throws IOException + { + this.nodeRef = ref; + this.conn = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + } + + public boolean equals(Object o) + { + if (o instanceof RepoTemplateSource) + { + return nodeRef.equals(((RepoTemplateSource)o).nodeRef); + } + else + { + return false; + } + } + + public int hashCode() + { + return nodeRef.hashCode(); + } + + public String toString() + { + return nodeRef.toString(); + } + + public long lastModified() + { + return conn.getLastModified(); + } + + public Reader getReader() throws IOException + { + inputStream = conn.getContentInputStream(); + return new InputStreamReader(inputStream); + } + + public void close() throws IOException + { + try + { + if (inputStream != null) + { + inputStream.close(); + } + } + finally + { + inputStream = null; + conn = null; + } + } + } +} diff --git a/source/java/org/alfresco/repo/template/ClassPathTemplateLoader.java b/source/java/org/alfresco/repo/template/ClassPathTemplateLoader.java new file mode 100644 index 0000000000..1ff995e663 --- /dev/null +++ b/source/java/org/alfresco/repo/template/ClassPathTemplateLoader.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.net.URL; + +import freemarker.cache.URLTemplateLoader; + +/** + * Custom FreeMarker template loader to locate templates stored on the ClassPath. + * + * @author Kevin Roast + */ +public class ClassPathTemplateLoader extends URLTemplateLoader +{ + /** + * @see freemarker.cache.URLTemplateLoader#getURL(java.lang.String) + */ + protected URL getURL(String name) + { + return this.getClass().getClassLoader().getResource(name); + } +} diff --git a/source/java/org/alfresco/repo/template/DateCompareMethod.java b/source/java/org/alfresco/repo/template/DateCompareMethod.java new file mode 100644 index 0000000000..d25905c51b --- /dev/null +++ b/source/java/org/alfresco/repo/template/DateCompareMethod.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.Date; +import java.util.List; + +import freemarker.ext.beans.BeanModel; +import freemarker.template.TemplateDateModel; +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNumberModel; + +/** + * @author Kevin Roast + * + * Custom FreeMarker Template language method. + *

    + * Compare two dates to see if they differ by the specified number of miliseconds + *

    + * Usage: + * dateCompare(dateA, dateB) - 1 if dateA if greater than dateB + * dateCompare(dateA, dateB, millis) - 1 if dateA is greater than dateB by at least millis, else 0 + */ +public final class DateCompareMethod implements TemplateMethodModelEx +{ + /** + * @see freemarker.template.TemplateMethodModel#exec(java.util.List) + */ + public Object exec(List args) throws TemplateModelException + { + int result = 0; + + if (args.size() >= 2) + { + Object arg0 = args.get(0); + Object arg1 = args.get(1); + long diff = 0; + if (args.size() == 3) + { + Object arg2 = args.get(2); + if (arg2 instanceof TemplateNumberModel) + { + Number number = ((TemplateNumberModel)arg2).getAsNumber(); + diff = number.longValue(); + } + } + if (arg0 instanceof TemplateDateModel && arg1 instanceof TemplateDateModel) + { + Date dateA = (Date)((TemplateDateModel)arg0).getAsDate(); + Date dateB = (Date)((TemplateDateModel)arg1).getAsDate(); + if (dateA.getTime() > (dateB.getTime() - diff)) + { + result = 1; + } + } + } + + return result; + } +} diff --git a/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java b/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java new file mode 100644 index 0000000000..06d81afef6 --- /dev/null +++ b/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.io.IOException; +import java.io.Writer; + +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.TemplateException; +import org.alfresco.service.cmr.repository.TemplateProcessor; +import org.apache.log4j.Logger; + +import freemarker.cache.MruCacheStorage; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateExceptionHandler; + +/** + * FreeMarker implementation the template processor interface + * + * @author Kevin Roast + */ +public class FreeMarkerProcessor implements TemplateProcessor +{ + private final static String MSG_ERROR_NO_TEMPLATE = "error_no_template"; + private final static String MSG_ERROR_TEMPLATE_FAIL = "error_template_fail"; + private final static String MSG_ERROR_TEMPLATE_IO = "error_template_io"; + + private static Logger logger = Logger.getLogger(FreeMarkerProcessor.class); + + /** FreeMarker processor configuration */ + private Configuration config = null; + + /** The permission-safe node service */ + private NodeService nodeService; + + /** The Content Service to use */ + private ContentService contentService; + + /** + * Set the node service + * + * @param nodeService The permission-safe node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the content service + * + * @param contentService The ContentService to use + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @return The FreeMarker config instance for this processor + */ + private Configuration getConfig() + { + if (this.config == null) + { + Configuration config = new Configuration(); + + // setup template cache + config.setCacheStorage(new MruCacheStorage(20, 0)); + + // use our custom loader to find templates on the ClassPath + config.setTemplateLoader(new ClassPathRepoTemplateLoader(nodeService, contentService)); + + // use our custom object wrapper that can deal with QNameMap objects directly + config.setObjectWrapper(new QNameAwareObjectWrapper()); + + // rethrow any exception so we can deal with them + config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + this.config = config; + } + return this.config; + } + + /** + * @see org.alfresco.service.cmr.repository.TemplateProcessor#process(java.lang.String, java.lang.Object, java.io.Writer) + */ + public void process(String template, Object model, Writer out) + { + if (template == null || template.length() == 0) + { + throw new IllegalArgumentException("Template name is mandatory."); + } + if (model == null) + { + throw new IllegalArgumentException("Model is mandatory."); + } + if (out == null) + { + throw new IllegalArgumentException("Output Writer is mandatory."); + } + + try + { + if (logger.isDebugEnabled()) + logger.debug("Executing template: " + template + " on model: " + model); + + Template t = getConfig().getTemplate(template); + if (t != null) + { + try + { + // perform the template processing against supplied data model + t.process(model, out); + } + catch (Throwable err) + { + throw new TemplateException(MSG_ERROR_TEMPLATE_FAIL, new Object[] {err.getMessage()}, err); + } + } + else + { + throw new TemplateException(MSG_ERROR_NO_TEMPLATE, new Object[] {template}); + } + } + catch (IOException ioerr) + { + throw new TemplateException(MSG_ERROR_TEMPLATE_IO, new Object[] {template}, ioerr); + } + } +} diff --git a/source/java/org/alfresco/repo/template/HasAspectMethod.java b/source/java/org/alfresco/repo/template/HasAspectMethod.java new file mode 100644 index 0000000000..02e4721449 --- /dev/null +++ b/source/java/org/alfresco/repo/template/HasAspectMethod.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.repository.TemplateNode; +import org.alfresco.service.namespace.QName; + +import freemarker.ext.beans.BeanModel; +import freemarker.ext.beans.StringModel; +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateScalarModel; + +/** + * @author Kevin Roast + * + * Custom FreeMarker Template language method. + *

    + * Method returns whether a TemplateNode has a particular aspect applied to it. The aspect + * name can be either the fully qualified QName or the short prefixed name string. + *

    + * Usage: hasAspect(TemplateNode node, String aspect) + */ +public final class HasAspectMethod implements TemplateMethodModelEx +{ + /** + * @see freemarker.template.TemplateMethodModel#exec(java.util.List) + */ + public Object exec(List args) throws TemplateModelException + { + int result = 0; + + if (args.size() == 2) + { + // arg 0 must be a wrapped TemplateNode object + BeanModel arg0 = (BeanModel)args.get(0); + + // arg 1 can be either wrapped QName object or a String + String arg1String = null; + Object arg1 = args.get(1); + if (arg1 instanceof BeanModel) + { + arg1String = ((BeanModel)arg1).getWrappedObject().toString(); + } + else if (arg1 instanceof TemplateScalarModel) + { + arg1String = ((TemplateScalarModel)arg1).getAsString(); + } + if (arg0.getWrappedObject() instanceof TemplateNode) + { + // test to see if this node has the aspect + if ( ((TemplateNode)arg0.getWrappedObject()).hasAspect(arg1String) ) + { + result = 1; + } + } + } + + return Integer.valueOf(result); + } +} diff --git a/source/java/org/alfresco/repo/template/I18NMessageMethod.java b/source/java/org/alfresco/repo/template/I18NMessageMethod.java new file mode 100644 index 0000000000..8fe9dc10ac --- /dev/null +++ b/source/java/org/alfresco/repo/template/I18NMessageMethod.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.List; + +import org.alfresco.i18n.I18NUtil; + +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateScalarModel; + +/** + * @author Kevin Roast + * + * Custom FreeMarker Template language method. + *

    + * Method an I18N message resolved for the current locale and specified message ID. + *

    + * Usage: message(String id) + */ +public final class I18NMessageMethod implements TemplateMethodModelEx +{ + /** + * @see freemarker.template.TemplateMethodModel#exec(java.util.List) + */ + public Object exec(List args) throws TemplateModelException + { + String result = ""; + + if (args.size() == 1) + { + Object arg0 = args.get(0); + if (arg0 instanceof TemplateScalarModel) + { + String id = ((TemplateScalarModel)arg0).getAsString(); + result = I18NUtil.getMessage(id); + } + } + + return result; + } +} diff --git a/source/java/org/alfresco/repo/template/NamePathResultsMap.java b/source/java/org/alfresco/repo/template/NamePathResultsMap.java new file mode 100644 index 0000000000..7dcdbd17f7 --- /dev/null +++ b/source/java/org/alfresco/repo/template/NamePathResultsMap.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.List; +import java.util.StringTokenizer; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.TemplateNode; + +/** + * A special Map that executes an XPath against the parent Node as part of the get() + * Map interface implementation. + * + * @author Kevin Roast + */ +public final class NamePathResultsMap extends BasePathResultsMap implements Cloneable +{ + /** + * Constructor + * + * @param parent The parent TemplateNode to execute searches from + * @param services The ServiceRegistry to use + */ + public NamePathResultsMap(TemplateNode parent, ServiceRegistry services) + { + super(parent, services); + } + + /** + * @see java.util.Map#get(java.lang.Object) + */ + public Object get(Object key) + { + StringBuilder xpath = new StringBuilder(128); + for (StringTokenizer t = new StringTokenizer(key.toString(), "/"); t.hasMoreTokens(); /**/) + { + if (xpath.length() != 0) + { + xpath.append('/'); + } + xpath.append("*[@cm:name='") + .append(t.nextToken()) // TODO: escape quotes? + .append("']"); + } + + List nodes = getChildrenByXPath(xpath.toString(), true); + return (nodes.size() != 0) ? nodes.get(0) : null; + } +} diff --git a/source/java/org/alfresco/repo/template/QNameAwareObjectWrapper.java b/source/java/org/alfresco/repo/template/QNameAwareObjectWrapper.java new file mode 100644 index 0000000000..ff935cc154 --- /dev/null +++ b/source/java/org/alfresco/repo/template/QNameAwareObjectWrapper.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.Map; + +import org.alfresco.service.namespace.QNameMap; + +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.ObjectWrapper; +import freemarker.template.SimpleHash; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +/** + * @author Kevin Roast + */ +public class QNameAwareObjectWrapper extends DefaultObjectWrapper +{ + /** + * Override to support wrapping of a QNameNodeMap by our custom wrapper object + */ + public TemplateModel wrap(Object obj) throws TemplateModelException + { + if (obj instanceof QNameMap) + { + return new QNameHash((QNameMap)obj, this); + } + else + { + return super.wrap(obj); + } + } + + + /** + * Inner class to support clone of QNameNodeMap + */ + class QNameHash extends SimpleHash + { + /** + * Constructor + * + * @param map + * @param wrapper + */ + public QNameHash(QNameMap map, ObjectWrapper wrapper) + { + super(map, wrapper); + } + + /** + * Override to support clone of a QNameNodeMap object + */ + protected Map copyMap(Map map) + { + if (map instanceof QNameMap) + { + return (Map)((QNameMap)map).clone(); + } + else + { + return super.copyMap(map); + } + } + } +} diff --git a/source/java/org/alfresco/repo/template/TemplateServiceImpl.java b/source/java/org/alfresco/repo/template/TemplateServiceImpl.java new file mode 100644 index 0000000000..defb56482d --- /dev/null +++ b/source/java/org/alfresco/repo/template/TemplateServiceImpl.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.TemplateException; +import org.alfresco.service.cmr.repository.TemplateProcessor; +import org.alfresco.service.cmr.repository.TemplateService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * @author Kevin Roast + */ +public class TemplateServiceImpl implements TemplateService, ApplicationContextAware +{ + private static Log logger = LogFactory.getLog(TemplateService.class); + + /** Spring ApplicationContext for bean lookup by ID */ + private ApplicationContext applicationContext; + + /** Default Template processor engine to use */ + private String defaultTemplateEngine; + + /** Available template engine names to impl class names */ + private Map templateEngines; + + /** Threadlocal instance for template processor cache */ + private static ThreadLocal> processors = new ThreadLocal(); + + /** + * Set the application context + * + * @param applicationContext the application context + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + /** + * @param defaultTemplateEngine The default Template Engine name to set. + */ + public void setDefaultTemplateEngine(String defaultTemplateEngine) + { + this.defaultTemplateEngine = defaultTemplateEngine; + } + + /** + * @param templateEngines The Map of template engine name to impl class name to set. + */ + public void setTemplateEngines(Map templateEngines) + { + this.templateEngines = templateEngines; + } + + /** + * @see org.alfresco.service.cmr.repository.TemplateService#getTemplateProcessor(java.lang.String) + */ + public TemplateProcessor getTemplateProcessor(String engine) + { + try + { + return getTemplateProcessorImpl(engine); + } + catch (Throwable err) + { + if (logger.isDebugEnabled()) + logger.debug("Unable to load template processor.", err); + + return null; + } + } + + /** + * @see org.alfresco.service.cmr.repository.TemplateService#processTemplate(java.lang.String, java.lang.String, java.lang.Object, java.io.Writer) + */ + public void processTemplate(String engine, String template, Object model, Writer out) + throws TemplateException + { + try + { + // execute template processor + TemplateProcessor processor = getTemplateProcessorImpl(engine); + processor.process(template, model, out); + } + catch (TemplateException terr) + { + throw terr; + } + catch (Throwable err) + { + throw new TemplateException(err.getMessage(), err); + } + } + + /** + * @see org.alfresco.service.cmr.repository.TemplateService#processTemplate(java.lang.String, java.lang.String, java.lang.Object) + */ + public String processTemplate(String engine, String template, Object model) + throws TemplateException + { + Writer out = new StringWriter(1024); + processTemplate(engine, template, model, out); + return out.toString(); + } + + /** + * Return the TemplateProcessor implementation for the named template engine + * + * @param name Template Engine name + * + * @return TemplateProcessor + */ + private TemplateProcessor getTemplateProcessorImpl(String name) + { + // use the ThreadLocal map to find the processors instance + // create the cache map for this thread if required + Map procMap = processors.get(); + if (procMap == null) + { + procMap = new HashMap(7, 1.0f); + processors.set(procMap); + } + + if (name == null) + { + name = defaultTemplateEngine; + } + + // find the impl for the named processor + TemplateProcessor processor = procMap.get(name); + if (processor == null) + { + String className = templateEngines.get(name); + if (className == null) + { + throw new AlfrescoRuntimeException("Unable to find configured ClassName for template engine: " + name); + } + try + { + Object obj; + try + { + obj = this.applicationContext.getBean(className); + } + catch (BeansException err) + { + // instantiate the processor class directory if not a Spring bean + obj = Class.forName(className).newInstance(); + } + + if (obj instanceof TemplateProcessor) + { + processor = (TemplateProcessor)obj; + } + else + { + throw new AlfrescoRuntimeException("Supplied template processors does not implement TemplateProcessor: " + className); + } + } + catch (ClassNotFoundException err1) + { + // if the bean is not a classname, then it may be a spring bean Id + throw new AlfrescoRuntimeException("Unable to load class for supplied template processors: " + className, err1); + } + catch (IllegalAccessException err2) + { + throw new AlfrescoRuntimeException("Unable to load class for supplied template processors: " + className, err2); + } + catch (InstantiationException err3) + { + throw new AlfrescoRuntimeException("Unable to instantiate class for supplied template processors: " + className, err3); + } + + // cache for later + procMap.put(name, processor); + } + + return processor; + } +} diff --git a/source/java/org/alfresco/repo/template/TemplateServiceImplTest.java b/source/java/org/alfresco/repo/template/TemplateServiceImplTest.java new file mode 100644 index 0000000000..ec73841c36 --- /dev/null +++ b/source/java/org/alfresco/repo/template/TemplateServiceImplTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.repo.dictionary.DictionaryComponent; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateNode; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @author Kevin Roast + */ +public class TemplateServiceImplTest extends TestCase +{ + private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private ContentService contentService; + private TemplateService templateService; + private NodeService nodeService; + private TransactionService transactionService; + private ServiceRegistry serviceRegistry; + private AuthenticationComponent authenticationComponent; + + /* + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception + { + super.setUp(); + + transactionService = (TransactionService)this.ctx.getBean("transactionComponent"); + contentService = (ContentService)this.ctx.getBean("contentService"); + nodeService = (NodeService)this.ctx.getBean("nodeService"); + templateService = (TemplateService)this.ctx.getBean("templateService"); + serviceRegistry = (ServiceRegistry)this.ctx.getBean("ServiceRegistry"); + + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + this.authenticationComponent.setSystemUserAsCurrentUser(); + + DictionaryDAO dictionaryDao = (DictionaryDAO)ctx.getBean("dictionaryDAO"); + + // load the system model + ClassLoader cl = BaseNodeServiceTest.class.getClassLoader(); + InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml"); + assertNotNull(modelStream); + M2Model model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + // load the test model + modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml"); + assertNotNull(modelStream); + model = M2Model.createModel(modelStream); + dictionaryDao.putModel(model); + + DictionaryComponent dictionary = new DictionaryComponent(); + dictionary.setDictionaryDAO(dictionaryDao); + BaseNodeServiceTest.loadModel(ctx); + } + + @Override + protected void tearDown() throws Exception + { + authenticationComponent.clearCurrentSecurityContext(); + super.tearDown(); + } + + public void testTemplates() + { + TransactionUtil.executeInUserTransaction( + transactionService, + new TransactionUtil.TransactionWork() + { + public Object doWork() throws Exception + { + StoreRef store = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "template_" + System.currentTimeMillis()); + NodeRef root = nodeService.getRootNode(store); + BaseNodeServiceTest.buildNodeGraph(nodeService, root); + + // check the default template engine exists + assertNotNull(templateService.getTemplateProcessor("freemarker")); + + // create test model + Map model = new HashMap(7, 1.0f); + + model.put("root", new TemplateNode(root, serviceRegistry, null)); + + // execute on test template + String output = templateService.processTemplate("freemarker", TEMPLATE_1, model); + + // check template contains the expected output + assertTrue( (output.indexOf(root.getId()) != -1) ); + + System.out.print(output); + + return null; + } + }); + } + + private static final String TEMPLATE_1 = "org/alfresco/repo/template/test_template1.ftl"; +} diff --git a/source/java/org/alfresco/repo/template/XPathResultsMap.java b/source/java/org/alfresco/repo/template/XPathResultsMap.java new file mode 100644 index 0000000000..9ececbd310 --- /dev/null +++ b/source/java/org/alfresco/repo/template/XPathResultsMap.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.TemplateNode; + +/** + * A special Map that executes an XPath against the parent Node as part of the get() + * Map interface implementation. + * + * @author Kevin Roast + */ +public final class XPathResultsMap extends BasePathResultsMap implements Cloneable +{ + /** + * Constructor + * + * @param parent The parent TemplateNode to execute searches from + * @param services The ServiceRegistry to use + */ + public XPathResultsMap(TemplateNode parent, ServiceRegistry services) + { + super(parent, services); + } + + /** + * @see java.util.Map#get(java.lang.Object) + */ + public Object get(Object key) + { + return getChildrenByXPath(key.toString(), false); + } +} diff --git a/source/java/org/alfresco/repo/template/test_template1.ftl b/source/java/org/alfresco/repo/template/test_template1.ftl new file mode 100644 index 0000000000..aa0ece4ce7 --- /dev/null +++ b/source/java/org/alfresco/repo/template/test_template1.ftl @@ -0,0 +1,54 @@ +
    Test Template 1
    + +<#-- Test basic properties --> +${root.id}
    +${root.name}
    +${root.properties?size}
    +${root.children?size}
    +<#if root.assocs["cm:translations"]?exists> +root.assocs
    + +${root.aspects?size}
    +<#if root.isContainer>root.isContainer
    +<#if root.isDocument>root.isDocumentr
    +<#--${root.content}
    --> +${root.url}
    +${root.displayPath}
    +${root.icon16}
    +${root.icon32}
    +<#if root.mimetype?exists>root.mimetype
    +<#if root.size?exists>root.size
    +<#if root.isLocked>root.isLocked
    + +<#-- Test child walking and property resolving --> + +<#list root.children as child> + <#-- show properties of each child --> + <#assign props = child.properties?keys> + <#list props as t> + <#-- If the property exists --> + <#if child.properties[t]?exists> + <#-- If it is a date, format it accordingly--> + <#if child.properties[t]?is_date> + + + <#-- If it is a boolean, format it accordingly--> + <#elseif child.properties[t]?is_boolean> + + + <#-- Otherwise treat it as a string --> + <#else> + + + + + + +
    ${t} = ${child.properties[t]?date}
    ${t} = ${child.properties[t]?string("yes", "no")}
    ${t} = ${child.properties[t]}
    + +<#-- Test XPath --> +<#list root.childrenByXPath["//*[@sys:store-protocol='workspace']"] as child> + ${child.name} + + +
    End Test Template 1
    diff --git a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionException.java b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionException.java new file mode 100644 index 0000000000..c7493e326f --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import org.springframework.transaction.TransactionException; + +/** + * Simple concrete implementation of the base class. + * + * @author Derek Hulley + */ +public class AlfrescoTransactionException extends TransactionException +{ + private static final long serialVersionUID = 3643033849898962687L; + + public AlfrescoTransactionException(String msg) + { + super(msg); + } + + public AlfrescoTransactionException(String msg, Throwable ex) + { + super(msg, ex); + } +} diff --git a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java new file mode 100644 index 0000000000..74a3a85d31 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.node.integrity.IntegrityChecker; +import org.alfresco.repo.search.impl.lucene.LuceneIndexerAndSearcherFactory; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.orm.hibernate3.SessionFactoryUtils; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Helper class to manage transaction synchronization. This provides helpers to + * ensure that the necessary TransactionSynchronization instances + * are registered on behalf of the application code. + * + * @author Derek Hulley + */ +public abstract class AlfrescoTransactionSupport +{ + /* + * The registrations of services is very explicit on the interface. This + * is to convey the idea that the execution of these services when the + * transaction completes is very explicit. As we only have a finite + * list of types of services that need registration, this is still + * OK. + */ + + /** + * The order of synchronization set to be 100 less than the Hibernate synchronization order + */ + public static final int SESSION_SYNCHRONIZATION_ORDER = + SessionFactoryUtils.SESSION_SYNCHRONIZATION_ORDER - 100; + + /** resource key to store the transaction synchronizer instance */ + private static final String RESOURCE_KEY_TXN_SYNCH = "txnSynch"; + + private static Log logger = LogFactory.getLog(AlfrescoTransactionSupport.class); + + /** + * Get a unique identifier associated with each transaction of each thread. Null is returned if + * no transaction is currently active. + * + * @return Returns the transaction ID, or null if no transaction is present + */ + public static String getTransactionId() + { + /* + * Go direct to the synchronizations as we don't want to register a resource if one doesn't exist. + * This method is heavily used, so the simple Map lookup on the ThreadLocal is the fastest. + */ + + TransactionSynchronizationImpl txnSynch = + (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (txnSynch == null) + { + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + // need to lazily register synchronizations + return registerSynchronizations().getTransactionId(); + } + else + { + return null; // not in a transaction + } + } + else + { + return txnSynch.getTransactionId(); + } + } + + /** + * Are there any pending changes which must be synchronized with the store? + * + * @return true => changes are pending + */ + public static boolean isDirty() + { + TransactionSynchronizationImpl synch = getSynchronization(); + + Set services = synch.getNodeDaoServices(); + for (NodeDaoService service : services) + { + if (service.isDirty()) + { + return true; + } + } + + return false; + } + + /** + * Gets a resource associated with the current transaction, which must be active. + *

    + * All necessary synchronization instances will be registered automatically, if required. + * + * + * @param key the thread resource map key + * @return Returns a thread resource of null if not present + */ + public static Object getResource(Object key) + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + // get the resource + Object resource = txnSynch.resources.get(key); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Fetched resource: \n" + + " key: " + key + "\n" + + " resource: " + resource); + } + return resource; + } + + /** + * Binds a resource to the current transaction, which must be active. + *

    + * All necessary synchronization instances will be registered automatically, if required. + * + * @param key + * @param resource + */ + public static void bindResource(Object key, Object resource) + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + // bind the resource + txnSynch.resources.put(key, resource); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bound resource: \n" + + " key: " + key + "\n" + + " resource: " + resource); + } + } + + /** + * Unbinds a resource from the current transaction, which must be active. + *

    + * All necessary synchronization instances will be registered automatically, if required. + * + * @param key + */ + public static void unbindResource(Object key) + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + // remove the resource + txnSynch.resources.remove(key); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Unbound resource: \n" + + " key: " + key); + } + } + + /** + * Method that registers a NodeDaoService against the transaction. + * Setting this will ensure that the pre- and post-commit operations perform + * the necessary cleanups against the NodeDaoService. + *

    + * This method can be called repeatedly as long as the service being bound + * implements equals and hashCode. + * + * @param nodeDaoService + */ + public static void bindNodeDaoService(NodeDaoService nodeDaoService) + { + // get transaction-local synchronization + TransactionSynchronizationImpl synch = getSynchronization(); + + // bind the service in + boolean bound = synch.getNodeDaoServices().add(nodeDaoService); + + // done + if (logger.isDebugEnabled()) + { + logBoundService(nodeDaoService, bound); + } + } + + /** + * Method that registers an IntegrityChecker against the transaction. + * Setting this will ensure that the pre- and post-commit operations perform + * the necessary cleanups against the IntegrityChecker. + *

    + * This method can be called repeatedly as long as the service being bound + * implements equals and hashCode. + * + * @param integrityChecker + */ + public static void bindIntegrityChecker(IntegrityChecker integrityChecker) + { + // get transaction-local synchronization + TransactionSynchronizationImpl synch = getSynchronization(); + + // bind the service in + boolean bound = synch.getIntegrityCheckers().add(integrityChecker); + + // done + if (logger.isDebugEnabled()) + { + logBoundService(integrityChecker, bound); + } + } + + /** + * Method that registers a LuceneIndexerAndSearcherFactory against + * the transaction. + *

    + * Setting this will ensure that the pre- and post-commit operations perform + * the necessary cleanups against the LuceneIndexerAndSearcherFactory. + *

    + * Although bound within a Set, it would still be better for the caller + * to only bind once per transaction, if possible. + * + * @param indexerAndSearcher the Lucene indexer to perform transaction completion + * tasks on + */ + public static void bindLucene(LuceneIndexerAndSearcherFactory indexerAndSearcher) + { + // get transaction-local synchronization + TransactionSynchronizationImpl synch = getSynchronization(); + + // bind the service in + boolean bound = synch.getLucenes().add(indexerAndSearcher); + + // done + if (logger.isDebugEnabled()) + { + logBoundService(indexerAndSearcher, bound); + } + } + + /** + * Method that registers a LuceneIndexerAndSearcherFactory against + * the transaction. + *

    + * Setting this will ensure that the pre- and post-commit operations perform + * the necessary cleanups against the LuceneIndexerAndSearcherFactory. + *

    + * Although bound within a Set, it would still be better for the caller + * to only bind once per transaction, if possible. + * + * @param indexerAndSearcher the Lucene indexer to perform transaction completion + * tasks on + */ + public static void bindListener(TransactionListener listener) + { + // get transaction-local synchronization + TransactionSynchronizationImpl synch = getSynchronization(); + + // bind the service in + boolean bound = synch.getListeners().add(listener); + + // done + if (logger.isDebugEnabled()) + { + logBoundService(listener, bound); + } + } + + /** + * Use as part of a debug statement + * + * @param service the service to report + * @param bound true if the service was just bound; false if it was previously bound + */ + private static void logBoundService(Object service, boolean bound) + { + if (bound) + { + logger.debug("Bound service: \n" + + " transaction: " + getTransactionId() + "\n" + + " service: " + service); + } + else + { + logger.debug("Service already bound: \n" + + " transaction: " + getTransactionId() + "\n" + + " service: " + service); + } + } + + /** + * Flush in-transaction resources. A transaction must be active. + *

    + * The flush may include: + *

      + *
    • {@link NodeDaoService#flush()}
    • + *
    • {@link RuleService#executePendingRules()}
    • + *
    • {@link IntegrityChecker#checkIntegrity()}
    • + *
    + * + */ + public static void flush() + { + // get transaction-local synchronization + TransactionSynchronizationImpl synch = getSynchronization(); + // flush + synch.flush(); + } + + /** + * Gets the current transaction synchronization instance, which contains the locally bound + * resources that are available to {@link #getResource(Object) retrieve} or + * {@link #bindResource(Object, Object) add to}. + *

    + * This method also ensures that the transaction binding has been performed. + * + * @return Returns the common synchronization instance used + */ + private static TransactionSynchronizationImpl getSynchronization() + { + // ensure synchronizations + registerSynchronizations(); + // get the txn synch instances + return (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + } + + /** + * Binds the Alfresco-specific to the transaction resources + * + * @return Returns the current or new synchronization implementation + */ + private static TransactionSynchronizationImpl registerSynchronizations() + { + /* + * No thread synchronization or locking required as the resources are all threadlocal + */ + if (!TransactionSynchronizationManager.isSynchronizationActive()) + { + throw new AlfrescoRuntimeException("Transaction must be active and synchronization is required"); + } + TransactionSynchronizationImpl txnSynch = + (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (txnSynch != null) + { + // synchronization already registered + return txnSynch; + } + // we need a unique ID for the transaction + StringBuilder sb = new StringBuilder(56); + sb.append(System.currentTimeMillis()).append(":").append(GUID.generate()); + String txnId = sb.toString(); + // register the synchronization + txnSynch = new TransactionSynchronizationImpl(txnId); + TransactionSynchronizationManager.registerSynchronization(txnSynch); + // register the resource that will ensure we don't duplication the synchronization + TransactionSynchronizationManager.bindResource(RESOURCE_KEY_TXN_SYNCH, txnSynch); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bound txn synch: " + txnSynch); + } + return txnSynch; + } + + /** + * Cleans out transaction resources if present + */ + private static void clearSynchronization() + { + if (TransactionSynchronizationManager.hasResource(RESOURCE_KEY_TXN_SYNCH)) + { + Object txnSynch = TransactionSynchronizationManager.unbindResource(RESOURCE_KEY_TXN_SYNCH); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Unbound txn synch:" + txnSynch); + } + } + } + + /** + * Helper method to rebind the synchronization to the transaction + * + * @param txnSynch + */ + private static void rebindSynchronization(TransactionSynchronizationImpl txnSynch) + { + TransactionSynchronizationManager.bindResource(RESOURCE_KEY_TXN_SYNCH, txnSynch); + if (logger.isDebugEnabled()) + { + logger.debug("Bound txn synch: " + txnSynch); + } + } + + /** + * Handler of txn synchronization callbacks specific to internal + * application requirements + */ + private static class TransactionSynchronizationImpl extends TransactionSynchronizationAdapter + { + private final String txnId; + private final Set nodeDaoServices; + private final Set integrityCheckers; + private final Set lucenes; + private final Set listeners; + private final Map resources; + + /** + * Sets up the resource map + * + * @param txnId + */ + public TransactionSynchronizationImpl(String txnId) + { + this.txnId = txnId; + nodeDaoServices = new HashSet(3); + integrityCheckers = new HashSet(3); + lucenes = new HashSet(3); + listeners = new HashSet(5); + resources = new HashMap(17); + } + + public String getTransactionId() + { + return txnId; + } + + /** + * @return Returns a set of NodeDaoService instances that will be called + * during end-of-transaction processing + */ + public Set getNodeDaoServices() + { + return nodeDaoServices; + } + + /** + * @return Returns a set of IntegrityChecker instances that will be called + * during end-of-transaction processing + */ + public Set getIntegrityCheckers() + { + return integrityCheckers; + } + + /** + * @return Returns a set of LuceneIndexerAndSearcherFactory that will be called + * during end-of-transaction processing + */ + public Set getLucenes() + { + return lucenes; + } + + /** + * @return Returns a set of TransactionListener instances that will be called + * during end-of-transaction processing + */ + public Set getListeners() + { + return listeners; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(50); + sb.append("TransactionSychronizationImpl") + .append("[ txnId=").append(txnId) + .append(", node service=").append(nodeDaoServices.size()) + .append(", integrity=").append(integrityCheckers.size()) + .append(", indexers=").append(lucenes.size()) + .append(", resources=").append(resources) + .append("]"); + return sb.toString(); + } + + /** + * Performs the in-transaction flushing. Typically done during a transaction or + * before commit. + */ + public void flush() + { + // check integrity + for (IntegrityChecker integrityChecker : integrityCheckers) + { + integrityChecker.checkIntegrity(); + } + // flush listeners + for (TransactionListener listener : listeners) + { + listener.flush(); + } + } + + /** + * @see AlfrescoTransactionSupport#SESSION_SYNCHRONIZATION_ORDER + */ + @Override + public int getOrder() + { + return AlfrescoTransactionSupport.SESSION_SYNCHRONIZATION_ORDER; + } + + @Override + public void suspend() + { + if (logger.isDebugEnabled()) + { + logger.debug("Suspending transaction: " + this); + } + AlfrescoTransactionSupport.clearSynchronization(); + } + + @Override + public void resume() + { + if (logger.isDebugEnabled()) + { + logger.debug("Resuming transaction: " + this); + } + AlfrescoTransactionSupport.rebindSynchronization(this); + } + + /** + * Pre-commit cleanup. + *

    + * Ensures that the session resources are {@link #flush() flushed}. + * The Lucene indexes are then prepared. + */ + @Override + public void beforeCommit(boolean readOnly) + { + if (logger.isDebugEnabled()) + { + logger.debug("Before commit " + (readOnly ? "read-only" : "" ) + ": " + this); + } + // get the txn ID + TransactionSynchronizationImpl synch = (TransactionSynchronizationImpl) + TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (synch == null) + { + throw new AlfrescoRuntimeException("No synchronization bound to thread"); + } + + // These are still considered part of the transaction so are executed here + for (TransactionListener listener : listeners) + { + listener.beforeCommit(readOnly); + } + + // flush + flush(); + // prepare the indexes + for (LuceneIndexerAndSearcherFactory lucene : lucenes) + { + lucene.prepare(); + } + } + + @Override + public void beforeCompletion() + { + if (logger.isDebugEnabled()) + { + logger.debug("Before completion: " + this); + } + // notify listeners + for (TransactionListener listener : listeners) + { + listener.beforeCompletion(); + } + } + + + @Override + public void afterCompletion(int status) + { + String statusStr = "unknown"; + switch (status) + { + case TransactionSynchronization.STATUS_COMMITTED: + statusStr = "committed"; + break; + case TransactionSynchronization.STATUS_ROLLED_BACK: + statusStr = "rolled-back"; + break; + default: + } + if (logger.isDebugEnabled()) + { + logger.debug("After completion (" + statusStr + "): " + this); + } + + // commit/rollback Lucene + for (LuceneIndexerAndSearcherFactory lucene : lucenes) + { + try + { + if (status == TransactionSynchronization.STATUS_COMMITTED) + { + lucene.commit(); + } + else + { + lucene.rollback(); + } + } + catch (RuntimeException e) + { + logger.error("After completion (" + statusStr + ") Lucene exception", e); + } + } + + // notify listeners + if (status == TransactionSynchronization.STATUS_COMMITTED) + { + for (TransactionListener listener : listeners) + { + try + { + listener.afterCommit(); + } + catch (RuntimeException e) + { + logger.error("After completion (" + statusStr + ") listener exception: \n" + + " listener: " + listener, + e); + } + } + } + else + { + for (TransactionListener listener : listeners) + { + try + { + listener.afterRollback(); + } + catch (RuntimeException e) + { + logger.error("After completion (" + statusStr + ") listener exception: \n" + + " listener: " + listener, + e); + } + } + } + + // clear the thread's registrations and synchronizations + AlfrescoTransactionSupport.clearSynchronization(); + } + } +} diff --git a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java new file mode 100644 index 0000000000..a4fd99ffcc --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * Tests integration between our UserTransaction implementation and + * our TransactionManager. + * + * @see org.alfresco.repo.transaction.AlfrescoTransactionManager + * @see org.alfresco.util.transaction.SpringAwareUserTransaction + * + * @author Derek Hulley + */ +public class AlfrescoTransactionSupportTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private ServiceRegistry serviceRegistry; + + public void setUp() throws Exception + { + serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + } + + public void testTransactionId() throws Exception + { + // get a user transaction + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction txn = transactionService.getUserTransaction(); + assertNull("Thread shouldn't have a txn ID", AlfrescoTransactionSupport.getTransactionId()); + + // begine the txn + txn.begin(); + String txnId = AlfrescoTransactionSupport.getTransactionId(); + assertNotNull("Expected thread to have a txn id", txnId); + + // check that it is threadlocal + Thread thread = new Thread(new Runnable() + { + public void run() + { + String txnId = AlfrescoTransactionSupport.getTransactionId(); + assertNull("New thread seeing txn id"); + } + }); + + // check that the txn id doesn't change + String txnIdCheck = AlfrescoTransactionSupport.getTransactionId(); + assertEquals("Transaction ID changed on same thread", txnId, txnIdCheck); + + // begin a new, inner transaction + { + UserTransaction txnInner = transactionService.getNonPropagatingUserTransaction(); + + String txnIdInner = AlfrescoTransactionSupport.getTransactionId(); + assertEquals("Inner transaction not started, so txn ID should not change", txnId, txnIdInner); + + // begin the nested txn + txnInner.begin(); + // check the ID for the outer transaction + txnIdInner = AlfrescoTransactionSupport.getTransactionId(); + assertNotSame("Inner txn ID must be different from outer txn ID", txnIdInner, txnId); + + // rollback the nested txn + txnInner.rollback(); + txnIdCheck = AlfrescoTransactionSupport.getTransactionId(); + assertEquals("Txn ID not popped inner txn completion", txnId, txnIdCheck); + } + + // rollback + txn.rollback(); + assertNull("Thread shouldn't have a txn ID after rollback", AlfrescoTransactionSupport.getTransactionId()); + + // start a new transaction + txn = transactionService.getUserTransaction(); + txn.begin(); + txnIdCheck = AlfrescoTransactionSupport.getTransactionId(); + assertNotSame("New transaction has same ID", txnId, txnIdCheck); + + // rollback + txn.rollback(); + assertNull("Thread shouldn't have a txn ID after rollback", AlfrescoTransactionSupport.getTransactionId()); + } + + public void testListener() throws Exception + { + final List strings = new ArrayList(1); + + // anonymous inner class to test it + TransactionListener listener = new TransactionListener() + { + public void flush() + { + strings.add("flush"); + } + public void beforeCommit(boolean readOnly) + { + strings.add("beforeCommit"); + } + public void beforeCompletion() + { + strings.add("beforeCompletion"); + } + public void afterCommit() + { + strings.add("afterCommit"); + } + public void afterRollback() + { + strings.add("afterRollback"); + } + }; + + // begin a transaction + TransactionService transactionService = serviceRegistry.getTransactionService(); + UserTransaction txn = transactionService.getUserTransaction(); + txn.begin(); + + // register it + AlfrescoTransactionSupport.bindListener(listener); + + // test flush + AlfrescoTransactionSupport.flush(); + assertTrue("flush not called on listener", strings.contains("flush")); + + // test commit + txn.commit(); + assertTrue("beforeCommit not called on listener", strings.contains("beforeCommit")); + assertTrue("beforeCompletion not called on listener", strings.contains("beforeCompletion")); + assertTrue("afterCommit not called on listener", strings.contains("afterCommit")); + } +} diff --git a/source/java/org/alfresco/repo/transaction/DummyTransactionService.java b/source/java/org/alfresco/repo/transaction/DummyTransactionService.java new file mode 100644 index 0000000000..28b9e263e8 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/DummyTransactionService.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.alfresco.service.transaction.TransactionService; + +/** + * Simple implementation of the transaction service that serve up + * entirely useless user transactions. It is useful within the context + * of some tests. + * + * @author Derek Hulley + */ +public class DummyTransactionService implements TransactionService +{ + private UserTransaction txn = new UserTransaction() + { + public void begin() {}; + public void commit() {}; + public int getStatus() {return Status.STATUS_NO_TRANSACTION;}; + public void rollback() {}; + public void setRollbackOnly() {}; + public void setTransactionTimeout(int arg0) {}; + }; + + public boolean isReadOnly() + { + return false; + } + + public UserTransaction getNonPropagatingUserTransaction() + { + return txn; + } + + public UserTransaction getUserTransaction() + { + return txn; + } + + public UserTransaction getUserTransaction(boolean readonly) + { + return txn; + } +} diff --git a/source/java/org/alfresco/repo/transaction/NodeDaoServiceTransactionInterceptor.java b/source/java/org/alfresco/repo/transaction/NodeDaoServiceTransactionInterceptor.java new file mode 100644 index 0000000000..406f766c85 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/NodeDaoServiceTransactionInterceptor.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.node.db.NodeDaoService; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.beans.factory.InitializingBean; + +/** + * Utility class that ensures that a NodeDaoService has been registered + * with the current transaction. + *

    + * It is designed to act as a postInterceptor on the NodeDaoService's + * {@link org.springframework.transaction.interceptor.TransactionProxyFactoryBean}. + * + * @author Derek Hulley + */ +public class NodeDaoServiceTransactionInterceptor implements MethodInterceptor, InitializingBean +{ + private NodeDaoService nodeDaoService; + + /** + * @param nodeDaoService the NodeDaoService to register + */ + public void setNodeDaoService(NodeDaoService nodeDaoService) + { + this.nodeDaoService = nodeDaoService; + } + + /** + * Checks that required values have been injected + */ + public void afterPropertiesSet() throws Exception + { + if (nodeDaoService == null) + { + throw new AlfrescoRuntimeException("NodeDaoService is required: " + this); + } + } + + public Object invoke(MethodInvocation invocation) throws Throwable + { + AlfrescoTransactionSupport.bindNodeDaoService(nodeDaoService); + // propogate the call + return invocation.proceed(); + } +} diff --git a/source/java/org/alfresco/repo/transaction/TransactionComponent.java b/source/java/org/alfresco/repo/transaction/TransactionComponent.java new file mode 100644 index 0000000000..9f4b2ae636 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/TransactionComponent.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import javax.transaction.UserTransaction; + +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.transaction.SpringAwareUserTransaction; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; + +/** + * Default implementation of Transaction Service + * + * @author David Caruana + */ +public class TransactionComponent implements TransactionService +{ + private PlatformTransactionManager transactionManager; + private boolean readOnly = false; + + /** + * Set the transaction manager to use + * + * @param transactionManager platform transaction manager + */ + public void setTransactionManager(PlatformTransactionManager transactionManager) + { + this.transactionManager = transactionManager; + } + + /** + * Set the read-only mode for all generated transactions. + * + * @param allowWrite false if all transactions must be read-only + */ + public void setAllowWrite(boolean allowWrite) + { + this.readOnly = !allowWrite; + } + + public boolean isReadOnly() + { + return readOnly; + } + + /** + * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRED + */ + public UserTransaction getUserTransaction() + { + SpringAwareUserTransaction txn = new SpringAwareUserTransaction( + transactionManager, + this.readOnly, + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRED, + TransactionDefinition.TIMEOUT_DEFAULT); + return txn; + } + + /** + * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRED + */ + public UserTransaction getUserTransaction(boolean readOnly) + { + SpringAwareUserTransaction txn = new SpringAwareUserTransaction( + transactionManager, + (readOnly | this.readOnly), + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRED, + TransactionDefinition.TIMEOUT_DEFAULT); + return txn; + } + + /** + * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRES_NEW + */ + public UserTransaction getNonPropagatingUserTransaction() + { + SpringAwareUserTransaction txn = new SpringAwareUserTransaction( + transactionManager, + this.readOnly, + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRES_NEW, + TransactionDefinition.TIMEOUT_DEFAULT); + return txn; + } +} diff --git a/source/java/org/alfresco/repo/transaction/TransactionComponentTest.java b/source/java/org/alfresco/repo/transaction/TransactionComponentTest.java new file mode 100644 index 0000000000..c1c5696a06 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/TransactionComponentTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * @see org.alfresco.repo.transaction.TransactionComponent + * + * @author Derek Hulley + */ +public class TransactionComponentTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private PlatformTransactionManager transactionManager; + private TransactionComponent transactionComponent; + private NodeService nodeService; + + public void setUp() throws Exception + { + transactionManager = (PlatformTransactionManager) ctx.getBean("transactionManager"); + transactionComponent = new TransactionComponent(); + transactionComponent.setTransactionManager(transactionManager); + transactionComponent.setAllowWrite(true); + + nodeService = (NodeService) ctx.getBean("dbNodeService"); + } + + public void testPropagatingTxn() throws Exception + { + // start a transaction + UserTransaction txnOuter = transactionComponent.getUserTransaction(); + txnOuter.begin(); + String txnIdOuter = AlfrescoTransactionSupport.getTransactionId(); + + // start a propagating txn + UserTransaction txnInner = transactionComponent.getUserTransaction(); + txnInner.begin(); + String txnIdInner = AlfrescoTransactionSupport.getTransactionId(); + + // the txn IDs should be the same + assertEquals("Txn ID not propagated", txnIdOuter, txnIdInner); + + // rollback the inner + txnInner.rollback(); + + // check both transactions' status + assertEquals("Inner txn not marked rolled back", Status.STATUS_ROLLEDBACK, txnInner.getStatus()); + assertEquals("Outer txn not marked for rolled back", Status.STATUS_MARKED_ROLLBACK, txnOuter.getStatus()); + + try + { + txnOuter.commit(); + fail("Outer txn not marked for rollback"); + } + catch (RollbackException e) + { + // expected + txnOuter.rollback(); + } + } + + public void testNonPropagatingTxn() throws Exception + { + // start a transaction + UserTransaction txnOuter = transactionComponent.getUserTransaction(); + txnOuter.begin(); + String txnIdOuter = AlfrescoTransactionSupport.getTransactionId(); + + // start a propagating txn + UserTransaction txnInner = transactionComponent.getNonPropagatingUserTransaction(); + txnInner.begin(); + String txnIdInner = AlfrescoTransactionSupport.getTransactionId(); + + // the txn IDs should be different + assertNotSame("Txn ID not propagated", txnIdOuter, txnIdInner); + + // rollback the inner + txnInner.rollback(); + + // outer should commit without problems + txnOuter.commit(); + } + + public void testReadOnlyTxn() throws Exception + { + // start a read-only transaction + transactionComponent.setAllowWrite(false); + + UserTransaction txn = transactionComponent.getUserTransaction(); + txn.begin(); + + // do some writing + try + { + nodeService.createStore( + StoreRef.PROTOCOL_WORKSPACE, + getName() + "_" + System.currentTimeMillis()); + txn.commit(); + fail("Read-only transaction wasn't detected"); + } + catch (InvalidDataAccessApiUsageException e) + { + int i = 0; + // expected + } + } +} diff --git a/source/java/org/alfresco/repo/transaction/TransactionListener.java b/source/java/org/alfresco/repo/transaction/TransactionListener.java new file mode 100644 index 0000000000..3e54b0d52f --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/TransactionListener.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +/** + * Listener for Alfresco-specific transaction callbacks. + * + * @see org.alfresco.repo.transaction.AlfrescoTransactionSupport + * + * @author Derek Hulley + */ +public interface TransactionListener +{ + /** + * Allows the listener to flush any consuming resources. This mechanism is + * used primarily during long-lived transactions to ensure that system resources + * are not used up. + */ + void flush(); + + /** + * Called before a transaction is committed. + *

    + * All transaction resources are still available. + * + * @param readOnly true if the transaction is read-only + */ + void beforeCommit(boolean readOnly); + + /** + * Invoked before transaction commit/rollback. Will be called after + * {@link #beforeCommit(boolean) } even if {@link #beforeCommit(boolean)} + * failed. + *

    + * Any exceptions generated here will cause the transaction to rollback. + *

    + * All transaction resources are still available. + */ + void beforeCompletion(); + + /** + * Invoked after transaction commit. + *

    + * Any exceptions generated here will cause the transaction to rollback. + *

    + * All transaction resources are still available. + */ + void afterCommit(); + + /** + * Invoked after transaction rollback. + *

    + * All transaction resources are still available. + */ + void afterRollback(); +} diff --git a/source/java/org/alfresco/repo/transaction/TransactionUtil.java b/source/java/org/alfresco/repo/transaction/TransactionUtil.java new file mode 100644 index 0000000000..e43a858907 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/TransactionUtil.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.transaction; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Class containing transactions helper methods and interfaces. + * + * @author Roy Wetherall + */ +public class TransactionUtil +{ + private static Log logger = LogFactory.getLog(TransactionUtil.class); + + /** + * Transaction work interface. + *

    + * This interface encapsulates a unit of work that should be done within a + * transaction. + */ + public interface TransactionWork + { + /** + * Method containing the work to be done in the user transaction. + * + * @return Return the result of the operation + */ + Result doWork() throws Exception; + } + + /** + * Flush transaction. + */ + public static void flush() + { + AlfrescoTransactionSupport.flush(); + } + + /** + * Execute the transaction work in a user transaction + * + * @param transactionService the transaction service + * @param transactionWork the transaction work + * + * @throws java.lang.RuntimeException if the transaction was rolled back + */ + public static R executeInUserTransaction( + TransactionService transactionService, + TransactionWork transactionWork) + { + return executeInTransaction(transactionService, transactionWork, false); + } + + /** + * Execute the transaction work in a non propigating user transaction + * + * @param transactionService the transaction service + * @param transactionWork the transaction work + * + * @throws java.lang.RuntimeException if the transaction was rolled back + */ + public static R executeInNonPropagatingUserTransaction( + TransactionService transactionService, + TransactionWork transactionWork) + { + return executeInTransaction(transactionService, transactionWork, true); + } + + /** + * Execute the transaction work in a user transaction of a specified type + * + * @param transactionService the transaction service + * @param transactionWork the transaction work + * @param ignoreException indicates whether errors raised in the work are + * ignored or re-thrown + * @param nonPropagatingUserTransaction indicates whether the transaction + * should be non propigating or not + * + * @throws java.lang.RuntimeException if the transaction was rolled back + */ + private static R executeInTransaction( + TransactionService transactionService, + TransactionWork transactionWork, + boolean nonPropagatingUserTransaction) + { + ParameterCheck.mandatory("transactionWork", transactionWork); + + R result = null; + + // Get the right type of user transaction + UserTransaction txn = null; + if (nonPropagatingUserTransaction == true) + { + txn = transactionService.getNonPropagatingUserTransaction(); + } + else + { + txn = transactionService.getUserTransaction(); + } + + try + { + // Begin the transaction, do the work and then commit the + // transaction + txn.begin(); + result = transactionWork.doWork(); + // rollback or commit + if (txn.getStatus() == Status.STATUS_MARKED_ROLLBACK) + { + // something caused the transaction to be marked for rollback + txn.rollback(); + } + else + { + // transaction should still commit + txn.commit(); + } + } + catch (Throwable exception) + { + try + { + // Roll back the exception + txn.rollback(); + } + catch (Throwable rollbackException) + { + // just dump the exception - we are already in a failure state + logger.error("Error rolling back transaction", rollbackException); + } + + // Re-throw the exception + if (exception instanceof RuntimeException) + { + throw (RuntimeException) exception; + } + else + { + throw new RuntimeException("Error during execution of transaction.", exception); + } + } + + return result; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/version/BaseVersionStoreTest.java b/source/java/org/alfresco/repo/version/BaseVersionStoreTest.java new file mode 100644 index 0000000000..2f2d414ce8 --- /dev/null +++ b/source/java/org/alfresco/repo/version/BaseVersionStoreTest.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.MutableAuthenticationDao; +import org.alfresco.repo.version.common.counter.VersionCounterDaoService; +import org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicy; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.TestWithUserUtils; + +public abstract class BaseVersionStoreTest extends BaseSpringTest +{ + /* + * Services used by the tests + */ + protected NodeService dbNodeService; + protected VersionService versionService; + protected VersionCounterDaoService versionCounterDaoService; + protected ContentService contentService; + protected DictionaryDAO dictionaryDAO; + protected AuthenticationService authenticationService; + protected TransactionService transactionService; + protected MutableAuthenticationDao authenticationDAO; + + /* + * Data used by tests + */ + protected StoreRef testStoreRef; + protected NodeRef rootNodeRef; + protected Map versionProperties; + protected HashMap nodeProperties; + + /** + * The most recent set of versionable nodes created by createVersionableNode + */ + protected HashMap versionableNodes; + + /* + * Proprety names and values + */ + protected static final String TEST_NAMESPACE = "http://www.alfresco.org/test/versionstorebasetest/1.0"; + protected static final QName TEST_TYPE_QNAME = QName.createQName(TEST_NAMESPACE, "testtype"); + protected static final QName TEST_ASPECT_QNAME = QName.createQName(TEST_NAMESPACE, "testaspect"); + protected static final QName PROP_1 = QName.createQName(TEST_NAMESPACE, "prop1"); + protected static final QName PROP_2 = QName.createQName(TEST_NAMESPACE, "prop2"); + protected static final QName PROP_3 = QName.createQName(TEST_NAMESPACE, "prop3"); + protected static final QName MULTI_PROP = QName.createQName(TEST_NAMESPACE, "multiProp"); + protected static final String VERSION_PROP_1 = "versionProp1"; + protected static final String VERSION_PROP_2 = "versionProp2"; + protected static final String VERSION_PROP_3 = "versionProp3"; + protected static final String VALUE_1 = "value1"; + protected static final String VALUE_2 = "value2"; + protected static final String VALUE_3 = "value3"; + protected static final QName TEST_CHILD_ASSOC_1 = QName.createQName(TEST_NAMESPACE, "childassoc1"); + protected static final QName TEST_CHILD_ASSOC_2 = QName.createQName(TEST_NAMESPACE, "childassoc2"); + protected static final QName TEST_ASSOC = QName.createQName(TEST_NAMESPACE, "assoc1"); + + protected Collection multiValue = null; + private AuthenticationComponent authenticationComponent; + protected static final String MULTI_VALUE_1 = "multi1"; + protected static final String MULTI_VALUE_2 = "multi2"; + + /** + * Test content + */ + protected static final String TEST_CONTENT = "This is the versioned test content."; + + /** + * Test user details + */ + private static final String PWD = "admin"; + private static final String USER_NAME = "admin"; + + /** + * Sets the meta model dao + * + * @param dictionaryDAO the meta model dao + */ + public void setDictionaryDAO(DictionaryDAO dictionaryDAO) + { + this.dictionaryDAO = dictionaryDAO; + } + + /** + * Called during the transaction setup + */ + protected void onSetUpInTransaction() throws Exception + { + // Set the multi value if required + if (this.multiValue == null) + { + this.multiValue = new ArrayList(); + this.multiValue.add(MULTI_VALUE_1); + this.multiValue.add(MULTI_VALUE_2); + } + + // Get the services by name from the application context + this.dbNodeService = (NodeService)applicationContext.getBean("dbNodeService"); + this.versionService = (VersionService)applicationContext.getBean("versionService"); + this.versionCounterDaoService = (VersionCounterDaoService)applicationContext.getBean("versionCounterDaoService"); + this.contentService = (ContentService)applicationContext.getBean("contentService"); + this.authenticationService = (AuthenticationService)applicationContext.getBean("authenticationService"); + this.authenticationComponent = (AuthenticationComponent)applicationContext.getBean("authenticationComponent"); + this.transactionService = (TransactionService)this.applicationContext.getBean("transactionComponent"); + this.authenticationDAO = (MutableAuthenticationDao) applicationContext.getBean("alfDaoImpl"); + + authenticationService.clearCurrentSecurityContext(); + + // Create the test model + createTestModel(); + + // Create a bag of properties for later use + this.versionProperties = new HashMap(); + versionProperties.put(VERSION_PROP_1, VALUE_1); + versionProperties.put(VERSION_PROP_2, VALUE_2); + versionProperties.put(VERSION_PROP_3, VALUE_3); + + // Create the node properties + this.nodeProperties = new HashMap(); + this.nodeProperties.put(PROP_1, VALUE_1); + this.nodeProperties.put(PROP_2, VALUE_2); + this.nodeProperties.put(PROP_3, VALUE_3); + this.nodeProperties.put(MULTI_PROP, (Serializable)multiValue); + this.nodeProperties.put(ContentModel.PROP_CONTENT, new ContentData(null, "text/plain", 0L, "UTF-8")); + + // Create a workspace that contains the 'live' nodes + this.testStoreRef = this.dbNodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + + // Get a reference to the root node + this.rootNodeRef = this.dbNodeService.getRootNode(this.testStoreRef); + + // Create an authenticate the user + + if(!authenticationDAO.userExists(USER_NAME)) + { + authenticationService.createAuthentication(USER_NAME, PWD.toCharArray()); + } + + TestWithUserUtils.authenticateUser(USER_NAME, PWD, this.rootNodeRef, this.authenticationService); + } + + /** + * Creates the test model used by the tests + */ + private void createTestModel() + { + InputStream is = getClass().getClassLoader().getResourceAsStream("org/alfresco/repo/version/VersionStoreBaseTest_model.xml"); + M2Model model = M2Model.createModel(is); + dictionaryDAO.putModel(model); + } + + /** + * Creates a new versionable node + * + * @return the node reference + */ + protected NodeRef createNewVersionableNode() + { + // Use this map to retrive the versionable nodes in later tests + this.versionableNodes = new HashMap(); + + // Create node (this node has some content) + NodeRef nodeRef = this.dbNodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}MyVersionableNode"), + TEST_TYPE_QNAME, + this.nodeProperties).getChildRef(); + this.dbNodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, new HashMap()); + + assertNotNull(nodeRef); + this.versionableNodes.put(nodeRef.getId(), nodeRef); + + // Add the content to the node + ContentWriter contentWriter = this.contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + contentWriter.putContent(TEST_CONTENT); + + // Add some children to the node + NodeRef child1 = this.dbNodeService.createNode( + nodeRef, + TEST_CHILD_ASSOC_1, + TEST_CHILD_ASSOC_1, + TEST_TYPE_QNAME, + this.nodeProperties).getChildRef(); + this.dbNodeService.addAspect(child1, ContentModel.ASPECT_VERSIONABLE, new HashMap()); + assertNotNull(child1); + this.versionableNodes.put(child1.getId(), child1); + NodeRef child2 = this.dbNodeService.createNode( + nodeRef, + TEST_CHILD_ASSOC_2, + TEST_CHILD_ASSOC_2, + TEST_TYPE_QNAME, + this.nodeProperties).getChildRef(); + this.dbNodeService.addAspect(child2, ContentModel.ASPECT_VERSIONABLE, new HashMap()); + assertNotNull(child2); + this.versionableNodes.put(child2.getId(), child2); + + // Create a node that can be associated with the root node + NodeRef assocNode = this.dbNodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}MyAssocNode"), + TEST_TYPE_QNAME, + this.nodeProperties).getChildRef(); + assertNotNull(assocNode); + this.dbNodeService.createAssociation(nodeRef, assocNode, TEST_ASSOC); + + return nodeRef; + } + + /** + * Creates a new version, checking the properties of the version. + *

    + * The default test propreties are assigned to the version. + * + * @param versionableNode the versionable node + * @return the created (and checked) new version + */ + protected Version createVersion(NodeRef versionableNode) + { + return createVersion(versionableNode, this.versionProperties); + } + + /** + * Creates a new version, checking the properties of the version. + * + * @param versionableNode the versionable node + * @param versionProperties the version properties + * @return the created (and checked) new version + */ + protected Version createVersion(NodeRef versionableNode, Map versionProperties) + { + // Get the next version number + int nextVersion = peekNextVersionNumber(); + String nextVersionLabel = peekNextVersionLabel(versionableNode, nextVersion, versionProperties); + + // Snap-shot the date-time + long beforeVersionTime = System.currentTimeMillis(); + + // Now lets create a new version for this node + Version newVersion = versionService.createVersion(versionableNode, this.versionProperties); + checkNewVersion(beforeVersionTime, nextVersion, nextVersionLabel, newVersion, versionableNode); + + // Return the new version + return newVersion; + } + + /** + * Gets the next version label + */ + protected String peekNextVersionLabel(NodeRef nodeRef, int versionNumber, Map versionProperties) + { + Version version = this.versionService.getCurrentVersion(nodeRef); + SerialVersionLabelPolicy policy = new SerialVersionLabelPolicy(); + return policy.calculateVersionLabel(ContentModel.TYPE_CMOBJECT, version, versionNumber, versionProperties); + } + + /** + * Checkd the validity of a new version + * + * @param beforeVersionTime the time snap shot before the version was created + * @param expectedVersionNumber the expected version number + * @param newVersion the new version + * @param versionableNode the versioned node + */ + protected void checkNewVersion(long beforeVersionTime, int expectedVersionNumber, String expectedVersionLabel, Version newVersion, NodeRef versionableNode) + { + assertNotNull(newVersion); + + // Check the version label and version number + assertEquals( + "The expected version number was not used.", + Integer.toString(expectedVersionNumber), + newVersion.getVersionProperty(VersionModel.PROP_VERSION_NUMBER).toString()); + assertEquals( + "The expected version label was not used.", + expectedVersionLabel, + newVersion.getVersionLabel()); + + // Check the created date + long afterVersionTime = System.currentTimeMillis(); + long createdDate = newVersion.getCreatedDate().getTime(); + if (createdDate < beforeVersionTime || createdDate > afterVersionTime) + { + fail("The created date of the version is incorrect."); + } + + // Check the creator + assertEquals(USER_NAME, newVersion.getCreator()); + + // Check the properties of the verison + Map props = newVersion.getVersionProperties(); + assertNotNull("The version properties collection should not be null.", props); + // TODO sort this out - need to check for the reserved properties too + //assertEquals(versionProperties.size(), props.size()); + for (String key : versionProperties.keySet()) + { + assertEquals( + versionProperties.get(key), + newVersion.getVersionProperty(key)); + } + + // Check that the node reference is correct + NodeRef nodeRef = newVersion.getFrozenStateNodeRef(); + assertNotNull(nodeRef); + assertEquals( + VersionModel.STORE_ID, + nodeRef.getStoreRef().getIdentifier()); + assertEquals( + VersionModel.STORE_PROTOCOL, + nodeRef.getStoreRef().getProtocol()); + assertNotNull(nodeRef.getId()); + + // TODO: How do we check the frozen attributes ?? + + // Check the node ref for the current version + String currentVersionLabel = (String)this.dbNodeService.getProperty( + versionableNode, + ContentModel.PROP_VERSION_LABEL); + assertEquals(newVersion.getVersionLabel(), currentVersionLabel); + } + + /** + * Returns the next version number without affecting the version counter. + * + * @return the next version number to be allocated + */ + protected int peekNextVersionNumber() + { + StoreRef lwVersionStoreRef = this.versionService.getVersionStoreReference(); + return this.versionCounterDaoService.currentVersionNumber(lwVersionStoreRef) + 1; + } + +} diff --git a/source/java/org/alfresco/repo/version/ContentServiceImplTest.java b/source/java/org/alfresco/repo/version/ContentServiceImplTest.java new file mode 100644 index 0000000000..3bf9e9cf44 --- /dev/null +++ b/source/java/org/alfresco/repo/version/ContentServiceImplTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.version.Version; + +/** + * Tests for retrieving frozen content from a verioned node + * + * @author Roy Wetherall + */ +public class ContentServiceImplTest extends BaseVersionStoreTest +{ + /** + * Test content data + */ + private final static String UPDATED_CONTENT = "This content has been updated with a new value."; + + /** + * The version content store + */ + private ContentService contentService; + + /** + * Called during the transaction setup + */ + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the instance of the required content service + this.contentService = (ContentService)this.applicationContext.getBean("contentService"); + } + + /** + * Test getReader + */ + public void testGetReader() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + NodeRef versionNodeRef = version.getFrozenStateNodeRef(); + + // Get the content reader for the frozen node + ContentReader contentReader = this.contentService.getReader(versionNodeRef, ContentModel.PROP_CONTENT); + assertNotNull(contentReader); + assertEquals(TEST_CONTENT, contentReader.getContentString()); + + // Now update the content and verison again + ContentWriter contentWriter = this.contentService.getWriter(versionableNode, ContentModel.PROP_CONTENT, true); + assertNotNull(contentWriter); + contentWriter.putContent(UPDATED_CONTENT); + Version version2 = createVersion(versionableNode, this.versionProperties); + NodeRef version2NodeRef = version2.getFrozenStateNodeRef(); + + // Get the content reader for the new verisoned content + ContentReader contentReader2 = this.contentService.getReader(version2NodeRef, ContentModel.PROP_CONTENT); + assertNotNull(contentReader2); + assertEquals(UPDATED_CONTENT, contentReader2.getContentString()); + } + + /** + * Test getWriter + */ + public void testGetWriter() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + // Get writer is not supported by the version content service + try + { + ContentWriter contentWriter = this.contentService.getWriter( + version.getFrozenStateNodeRef(), + ContentModel.PROP_CONTENT, + true); + contentWriter.putContent("bobbins"); + fail("This operation is not supported."); + } + catch (Exception exception) + { + // An exception should be raised + } + } +} diff --git a/source/java/org/alfresco/repo/version/NodeServiceImpl.java b/source/java/org/alfresco/repo/version/NodeServiceImpl.java new file mode 100644 index 0000000000..8a0828aa9b --- /dev/null +++ b/source/java/org/alfresco/repo/version/NodeServiceImpl.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidAspectException; +import org.alfresco.service.cmr.repository.AssociationExistsException; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.NodeRef.Status; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; + + +/** + * The light weight version store node service implementation. + * + * @author Roy Wetherall + */ +public class NodeServiceImpl implements NodeService, VersionModel +{ + /** + * Error messages + */ + private final static String MSG_UNSUPPORTED = + "This operation is not supported by a version store implementation of the node service."; + + /** + * The name of the spoofed root association + */ + private static final QName rootAssocName = QName.createQName(VersionModel.NAMESPACE_URI, "versionedState"); + + /** + * The db node service, used as the version store implementation + */ + protected NodeService dbNodeService; + + /** + * The repository searcher + */ + @SuppressWarnings("unused") + private SearchService searcher; + + /** + * The dictionary service + */ + protected DictionaryService dicitionaryService; + + + /** + * Sets the db node service, used as the version store implementation + * + * @param nodeService the node service + */ + public void setDbNodeService(NodeService nodeService) + { + this.dbNodeService = nodeService; + } + + /** + * Sets the searcher + * + * @param searcher the searcher + */ + public void setSearcher(SearchService searcher) + { + this.searcher = searcher; + } + + /** + * Sets the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dicitionaryService = dictionaryService; + } + + /** + * Delegates to the NodeService used as the version store implementation + */ + public List getStores() + { + return dbNodeService.getStores(); + } + + /** + * Delegates to the NodeService used as the version store implementation + */ + public StoreRef createStore(String protocol, String identifier) + { + return dbNodeService.createStore(protocol, identifier); + } + + /** + * Delegates to the NodeService used as the version store implementation + */ + public boolean exists(StoreRef storeRef) + { + return dbNodeService.exists(storeRef); + } + + /** + * Delegates to the NodeService used as the version store implementation + */ + public boolean exists(NodeRef nodeRef) + { + return dbNodeService.exists(convertNodeRef(nodeRef)); + } + + /** + * Delegates to the NodeService used as the version store implementation + */ + public Status getNodeStatus(NodeRef nodeRef) + { + return dbNodeService.getNodeStatus(nodeRef); + } + + /** + * Convert the incomming node ref (with the version store protocol specified) + * to the internal representation with the workspace protocol. + * + * @param nodeRef the incomming verison protocol node reference + * @return the internal version node reference + */ + private NodeRef convertNodeRef(NodeRef nodeRef) + { + return new NodeRef(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, STORE_ID), nodeRef.getId()); + } + + /** + * Delegates to the NodeService used as the version store implementation + */ + public NodeRef getRootNode(StoreRef storeRef) + { + return dbNodeService.getRootNode(storeRef); + } + + /** + * @throws UnsupportedOperationException always + */ + public ChildAssociationRef createNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public ChildAssociationRef createNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName, + Map properties) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public ChildAssociationRef addChild(NodeRef parentRef, + NodeRef childRef, + QName assocTypeQName, + QName qname) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public ChildAssociationRef moveNode(NodeRef nodeToMoveRef, NodeRef newParentRef, QName assocTypeQName, QName assocQName) throws InvalidNodeRefException + { + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index) throws InvalidChildAssociationRefException + { + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * Type translation for version store + */ + public QName getType(NodeRef nodeRef) throws InvalidNodeRefException + { + return (QName)this.dbNodeService.getProperty(convertNodeRef(nodeRef), PROP_QNAME_FROZEN_NODE_TYPE); + } + + /** + * @see org.alfresco.service.cmr.repository.NodeService#setType(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException + { + // This operation is not supported for a version store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public void addAspect(NodeRef nodeRef, QName aspectRef, Map aspectProperties) throws InvalidNodeRefException, InvalidAspectException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * Translation for version store + */ + public boolean hasAspect(NodeRef nodeRef, QName aspectRef) throws InvalidNodeRefException, InvalidAspectException + { + return getAspects(nodeRef).contains(aspectRef); + } + + /** + * @throws UnsupportedOperationException always + */ + public void removeAspect(NodeRef nodeRef, QName aspectRef) throws InvalidNodeRefException, InvalidAspectException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * Translation for version store + */ + public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException + { + return new HashSet( + (ArrayList)this.dbNodeService.getProperty(convertNodeRef(nodeRef), PROP_QNAME_FROZEN_ASPECTS)); + } + + /** + * Property translation for version store + */ + public Map getProperties(NodeRef nodeRef) throws InvalidNodeRefException + { + Map result = new HashMap(); + + // TODO should be doing this using a path query .. + + Collection children = this.dbNodeService.getChildAssocs(convertNodeRef(nodeRef)); + for (ChildAssociationRef child : children) + { + if (child.getQName().equals(CHILD_QNAME_VERSIONED_ATTRIBUTES)) + { + NodeRef versionedAttribute = child.getChildRef(); + + // Get the QName and the value + Serializable value = null; + QName qName = (QName)this.dbNodeService.getProperty(versionedAttribute, PROP_QNAME_QNAME); + Boolean isMultiValue = (Boolean)this.dbNodeService.getProperty(versionedAttribute, PROP_QNAME_IS_MULTI_VALUE); + if (isMultiValue.booleanValue() == false) + { + value = this.dbNodeService.getProperty(versionedAttribute, PROP_QNAME_VALUE); + } + else + { + value = this.dbNodeService.getProperty(versionedAttribute, PROP_QNAME_MULTI_VALUE); + } + + result.put(qName, value); + } + } + + return result; + } + + /** + * Property translation for version store + */ + public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException + { + // TODO should be doing this with a search ... + + Map properties = getProperties(convertNodeRef(nodeRef)); + return properties.get(qname); + } + + /** + * @throws UnsupportedOperationException always + */ + public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public void setProperty(NodeRef nodeRef, QName qame, Serializable value) throws InvalidNodeRefException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * The node will appear to be attached to the root of the version store + * + * @see NodeService#getParentAssocs(NodeRef) + */ + public List getParentAssocs(NodeRef nodeRef) + { + return getParentAssocs(nodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL); + } + + /** + * The node will apprear to be attached to the root of the version store + * + * @see NodeService#getParentAssocs(NodeRef, QNamePattern, QNamePattern) + */ + public List getParentAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern, QNamePattern qnamePattern) + { + List result = new ArrayList(); + if (qnamePattern.isMatch(rootAssocName) == true) + { + result.add(new ChildAssociationRef( + ContentModel.ASSOC_CHILDREN, + dbNodeService.getRootNode(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, STORE_ID)), + rootAssocName, + nodeRef)); + } + return result; + } + + /** + * @see RegexQNamePattern#MATCH_ALL + * @see #getChildAssocs(NodeRef, QNamePattern, QNamePattern) + */ + public List getChildAssocs(NodeRef nodeRef) throws InvalidNodeRefException + { + return getChildAssocs(convertNodeRef(nodeRef), RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL); + } + + /** + * Performs conversion from version store properties to real associations + */ + public List getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern, QNamePattern qnamePattern) throws InvalidNodeRefException + { + // Get the child assocs from the version store + List childAssocRefs = this.dbNodeService.getChildAssocs( + convertNodeRef(nodeRef), + RegexQNamePattern.MATCH_ALL, CHILD_QNAME_VERSIONED_CHILD_ASSOCS); + List result = new ArrayList(childAssocRefs.size()); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + // Get the child reference + NodeRef childRef = childAssocRef.getChildRef(); + NodeRef referencedNode = (NodeRef)this.dbNodeService.getProperty(childRef, ContentModel.PROP_REFERENCE); + + // get the qualified name of the frozen child association and filter out unwanted names + QName qName = (QName)this.dbNodeService.getProperty(childRef, PROP_QNAME_ASSOC_QNAME); + + if (qnamePattern.isMatch(qName) == true) + { + // Retrieve the isPrimary and nthSibling values of the forzen child association + QName assocType = (QName)this.dbNodeService.getProperty(childRef, PROP_QNAME_ASSOC_TYPE_QNAME); + boolean isPrimary = ((Boolean)this.dbNodeService.getProperty(childRef, PROP_QNAME_IS_PRIMARY)).booleanValue(); + int nthSibling = ((Integer)this.dbNodeService.getProperty(childRef, PROP_QNAME_NTH_SIBLING)).intValue(); + + // Build a child assoc ref to add to the returned list + ChildAssociationRef newChildAssocRef = new ChildAssociationRef( + assocType, + nodeRef, + qName, + referencedNode, + isPrimary, + nthSibling); + result.add(newChildAssocRef); + } + } + + // sort the results so that the order appears to be exactly as it was originally + Collections.sort(result); + + return result; + } + + /** + * Simulates the node begin attached ot the root node of the version store. + */ + public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException + { + return new ChildAssociationRef( + ContentModel.ASSOC_CHILDREN, + dbNodeService.getRootNode(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, STORE_ID)), + rootAssocName, + nodeRef); + } + + /** + * @throws UnsupportedOperationException always + */ + public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) + throws InvalidNodeRefException, AssociationExistsException + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public List getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern) + { + // Get the child assocs from the version store + List childAssocRefs = this.dbNodeService.getChildAssocs( + convertNodeRef(sourceRef), + RegexQNamePattern.MATCH_ALL, CHILD_QNAME_VERSIONED_ASSOCS); + List result = new ArrayList(childAssocRefs.size()); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + // Get the assoc reference + NodeRef childRef = childAssocRef.getChildRef(); + NodeRef referencedNode = (NodeRef)this.dbNodeService.getProperty(childRef, ContentModel.PROP_REFERENCE); + + // get the qualified type name of the frozen child association and filter out unwanted names + QName qName = (QName)this.dbNodeService.getProperty(childRef, PROP_QNAME_ASSOC_TYPE_QNAME); + + if (qnamePattern.isMatch(qName) == true) + { + AssociationRef newAssocRef = new AssociationRef(sourceRef, qName, referencedNode); + result.add(newAssocRef); + } + } + + return result; + } + + /** + * @throws UnsupportedOperationException always + */ + public List getSourceAssocs(NodeRef sourceRef, QNamePattern qnamePattern) + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + /** + * @throws UnsupportedOperationException always + */ + public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException + { + ChildAssociationRef childAssocRef = getPrimaryParent(nodeRef); + Path path = new Path(); + path.append(new Path.ChildAssocElement(childAssocRef)); + return path; + } + + /** + * @throws UnsupportedOperationException always + */ + public List getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException + { + List paths = new ArrayList(1); + paths.add(getPath(nodeRef)); + return paths; + } + + public List selectNodes(NodeRef contextNode, String XPath, QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + public List selectProperties(NodeRef contextNode, String XPath, QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + + public boolean contains(NodeRef nodeRef, QName property, String sqlLikePattern) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + public boolean like(NodeRef nodeRef, QName property, String sqlLikePattern, boolean includeFTS) + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + +} diff --git a/source/java/org/alfresco/repo/version/NodeServiceImplTest.java b/source/java/org/alfresco/repo/version/NodeServiceImplTest.java new file mode 100644 index 0000000000..e519177248 --- /dev/null +++ b/source/java/org/alfresco/repo/version/NodeServiceImplTest.java @@ -0,0 +1,568 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.debug.NodeStoreInspector; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Roy Wetherall + */ +public class NodeServiceImplTest extends BaseVersionStoreTest +{ + private static Log logger = LogFactory.getLog(NodeServiceImplTest.class); + + /** + * Light weight version store node service + */ + protected NodeService lightWeightVersionStoreNodeService = null; + + /** + * Error message + */ + private final static String MSG_ERR = + "This operation is not supported by a version store implementation of the node service."; + + /** + * Dummy data used in failure tests + */ + private NodeRef dummyNodeRef = null; + private QName dummyQName = null; + + /** + * Called during the transaction setup + */ + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the node service by name + this.lightWeightVersionStoreNodeService = (NodeService)this.applicationContext.getBean("versionNodeService"); + + // Create some dummy data used during the tests + this.dummyNodeRef = new NodeRef( + this.versionService.getVersionStoreReference(), + "dummy"); + this.dummyQName = QName.createQName("{dummy}dummy"); + } + + /** + * Test getType + */ + public void testGetType() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + // Get the type from the versioned state + QName versionedType = this.lightWeightVersionStoreNodeService.getType(version.getFrozenStateNodeRef()); + assertNotNull(versionedType); + assertEquals(this.dbNodeService.getType(versionableNode), versionedType); + } + + /** + * Test getProperties + */ + public void testGetProperties() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Get a list of the nodes properties + Map origProps = this.dbNodeService.getProperties(versionableNode); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + // Get the properties of the versioned state + Map versionedProperties = this.lightWeightVersionStoreNodeService.getProperties(version.getFrozenStateNodeRef()); + //assertEquals(origProps.size(), versionedProperties.size()); + for (QName key : origProps.keySet()) + { + assertTrue(versionedProperties.containsKey(key)); + assertEquals(origProps.get(key), versionedProperties.get(key)); + } + + // TODO do futher versioning and check by changing values + } + + /** + * Test getProperty + */ + public void testGetProperty() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + // Check the property values can be retrieved + Serializable value1 = this.lightWeightVersionStoreNodeService.getProperty( + version.getFrozenStateNodeRef(), + PROP_1); + assertEquals(VALUE_1, value1); + + // Check the multi values property specifically + Collection multiValue = (Collection)this.lightWeightVersionStoreNodeService.getProperty(version.getFrozenStateNodeRef(), MULTI_PROP); + assertNotNull(multiValue); + assertEquals(2, multiValue.size()); + String[] array = multiValue.toArray(new String[multiValue.size()]); + assertEquals(MULTI_VALUE_1, array[0]); + assertEquals(MULTI_VALUE_2, array[1]); + } + + /** + * Test getChildAssocs + */ + public void testGetChildAssocs() + { + if (logger.isDebugEnabled()) + { + // Let's have a look at the version store .. + System.out.println(NodeStoreInspector.dumpNodeStore( + this.dbNodeService, + this.versionService.getVersionStoreReference()) + "\n\n"); + logger.debug(""); + } + + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + Collection origionalChildren = this.dbNodeService.getChildAssocs(versionableNode); + assertNotNull(origionalChildren); + + // Store the origional children in a map for easy navigation later + HashMap origionalChildAssocRefs = new HashMap(); + for (ChildAssociationRef ref : origionalChildren) + { + origionalChildAssocRefs.put(ref.getChildRef().getId(), ref); + } + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + if (logger.isDebugEnabled()) + { + // Let's have a look at the version store .. + System.out.println(NodeStoreInspector.dumpNodeStore( + this.dbNodeService, + this.versionService.getVersionStoreReference())); + } + + // Get the children of the versioned node + Collection versionedChildren = this.lightWeightVersionStoreNodeService.getChildAssocs(version.getFrozenStateNodeRef()); + assertNotNull(versionedChildren); + assertEquals(origionalChildren.size(), versionedChildren.size()); + + for (ChildAssociationRef versionedChildRef : versionedChildren) + { + ChildAssociationRef origChildAssocRef = origionalChildAssocRefs.get(versionedChildRef.getChildRef().getId()); + assertNotNull(origChildAssocRef); + + assertEquals( + origChildAssocRef.getChildRef(), + versionedChildRef.getChildRef()); + assertEquals( + origChildAssocRef.isPrimary(), + versionedChildRef.isPrimary()); + assertEquals( + origChildAssocRef.getNthSibling(), + versionedChildRef.getNthSibling()); + } + } + + /** + * Test getAssociationTargets + */ + public void testGetAssociationTargets() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Store the current details of the target associations + List origAssocs = this.dbNodeService.getTargetAssocs( + versionableNode, + RegexQNamePattern.MATCH_ALL); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + List assocs = this.lightWeightVersionStoreNodeService.getTargetAssocs( + version.getFrozenStateNodeRef(), + RegexQNamePattern.MATCH_ALL); + assertNotNull(assocs); + assertEquals(origAssocs.size(), assocs.size()); + } + + /** + * Test hasAspect + */ + public void testHasAspect() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + boolean test1 = this.lightWeightVersionStoreNodeService.hasAspect( + version.getFrozenStateNodeRef(), + ContentModel.ASPECT_UIFACETS); + assertFalse(test1); + + boolean test2 = this.lightWeightVersionStoreNodeService.hasAspect( + version.getFrozenStateNodeRef(), + ContentModel.ASPECT_VERSIONABLE); + assertTrue(test2); + } + + /** + * Test getAspects + */ + public void testGetAspects() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + Set origAspects = this.dbNodeService.getAspects(versionableNode); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + + Set aspects = this.lightWeightVersionStoreNodeService.getAspects(version.getFrozenStateNodeRef()); + assertEquals(origAspects.size(), aspects.size()); + + // TODO check that the set's contain the same items + } + + /** + * Test getParentAssocs + */ + public void testGetParentAssocs() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + NodeRef nodeRef = version.getFrozenStateNodeRef(); + + List results = this.lightWeightVersionStoreNodeService.getParentAssocs(nodeRef); + assertNotNull(results); + assertEquals(1, results.size()); + ChildAssociationRef childAssoc = results.get(0); + assertEquals(nodeRef, childAssoc.getChildRef()); + NodeRef versionStoreRoot = this.dbNodeService.getRootNode(this.versionService.getVersionStoreReference()); + assertEquals(versionStoreRoot, childAssoc.getParentRef()); + } + + /** + * Test getPrimaryParent + */ + public void testGetPrimaryParent() + { + // Create a new versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Create a new version + Version version = createVersion(versionableNode, this.versionProperties); + NodeRef nodeRef = version.getFrozenStateNodeRef(); + + ChildAssociationRef childAssoc = this.lightWeightVersionStoreNodeService.getPrimaryParent(nodeRef); + assertNotNull(childAssoc); + assertEquals(nodeRef, childAssoc.getChildRef()); + NodeRef versionStoreRoot = this.dbNodeService.getRootNode(this.versionService.getVersionStoreReference()); + assertEquals(versionStoreRoot, childAssoc.getParentRef()); + } + + /** ================================================ + * These test ensure that the following operations + * are not supported as expected. + */ + + /** + * Test createNode + */ + public void testCreateNode() + { + try + { + this.lightWeightVersionStoreNodeService.createNode( + dummyNodeRef, + null, + dummyQName, + ContentModel.TYPE_CONTENT); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test addAspect + */ + public void testAddAspect() + { + try + { + this.lightWeightVersionStoreNodeService.addAspect( + dummyNodeRef, + TEST_ASPECT_QNAME, + null); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test removeAspect + */ + public void testRemoveAspect() + { + try + { + this.lightWeightVersionStoreNodeService.removeAspect( + dummyNodeRef, + TEST_ASPECT_QNAME); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test delete node + */ + public void testDeleteNode() + { + try + { + this.lightWeightVersionStoreNodeService.deleteNode(this.dummyNodeRef); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test addChild + */ + public void testAddChild() + { + try + { + this.lightWeightVersionStoreNodeService.addChild( + this.dummyNodeRef, + this.dummyNodeRef, + this.dummyQName, + this.dummyQName); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test removeChild + */ + public void testRemoveChild() + { + try + { + this.lightWeightVersionStoreNodeService.removeChild( + this.dummyNodeRef, + this.dummyNodeRef); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test setProperties + */ + public void testSetProperties() + { + try + { + this.lightWeightVersionStoreNodeService.setProperties( + this.dummyNodeRef, + new HashMap()); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test setProperty + */ + public void testSetProperty() + { + try + { + this.lightWeightVersionStoreNodeService.setProperty( + this.dummyNodeRef, + this.dummyQName, + "dummy"); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test createAssociation + */ + public void testCreateAssociation() + { + try + { + this.lightWeightVersionStoreNodeService.createAssociation( + this.dummyNodeRef, + this.dummyNodeRef, + this.dummyQName); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test removeAssociation + */ + public void testRemoveAssociation() + { + try + { + this.lightWeightVersionStoreNodeService.removeAssociation( + this.dummyNodeRef, + this.dummyNodeRef, + this.dummyQName); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test getAssociationSources + */ + public void testGetAssociationSources() + { + try + { + this.lightWeightVersionStoreNodeService.getSourceAssocs( + this.dummyNodeRef, + this.dummyQName); + fail("This operation is not supported."); + } + catch (UnsupportedOperationException exception) + { + if (exception.getMessage() != MSG_ERR) + { + fail("Unexpected exception raised during method excution: " + exception.getMessage()); + } + } + } + + /** + * Test getPath + */ + public void testGetPath() + { + Path path = this.lightWeightVersionStoreNodeService.getPath(this.dummyNodeRef); + } + + /** + * Test getPaths + */ + public void testGetPaths() + { + List paths = this.lightWeightVersionStoreNodeService.getPaths(this.dummyNodeRef, false); + } +} diff --git a/source/java/org/alfresco/repo/version/VersionBootstrap.java b/source/java/org/alfresco/repo/version/VersionBootstrap.java new file mode 100644 index 0000000000..46226e5e23 --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionBootstrap.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.transaction.TransactionService; + +/** + * Bootstrap Version Store + * + * @author David Caruana + */ +public class VersionBootstrap +{ + private TransactionService transactionService; + private NodeService nodeService; + private AuthenticationComponent authenticationComponent; + private PermissionService permissionService; + + + /** + * Sets the Transaction Service + * + * @param userTransaction the user transaction + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Sets the Node Service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * Bootstrap the Version Store + */ + public void bootstrap() + { + UserTransaction userTransaction = transactionService.getUserTransaction(); + authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); + + try + { + userTransaction.begin(); + + // Ensure that the version store has been created + if (this.nodeService.exists(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, VersionModel.STORE_ID)) == true) + { + userTransaction.rollback(); + } + else + { + StoreRef vStore = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, VersionModel.STORE_ID); + // TODO: For now there are no permissions on version access + permissionService.setPermission(nodeService.getRootNode(vStore), permissionService.getAllAuthorities(), permissionService.getAllPermission(), true); + userTransaction.commit(); + } + } + catch(Throwable e) + { + // rollback the transaction + try { if (userTransaction != null) {userTransaction.rollback();} } catch (Exception ex) {} + try {authenticationComponent.clearCurrentSecurityContext(); } catch (Exception ex) {} + throw new AlfrescoRuntimeException("Bootstrap failed", e); + } + finally + { + authenticationComponent.clearCurrentSecurityContext(); + } + } + +} diff --git a/source/java/org/alfresco/repo/version/VersionModel.java b/source/java/org/alfresco/repo/version/VersionModel.java new file mode 100644 index 0000000000..9942a0a10f --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionModel.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.QName; + +/** + * interface conating the constants used by the light weight + * version store implementation + * + * @author Roy Wetherall + */ +public interface VersionModel +{ + /** + * Namespace + */ + public static final String NAMESPACE_URI = "http://www.alfresco.org/model/versionstore/1.0"; + + /** + * The store protocol + */ + public static final String STORE_PROTOCOL = VersionService.VERSION_STORE_PROTOCOL; + + /** + * The store id + */ + public static final String STORE_ID = "lightWeightVersionStore"; + + + public static final String PROP_VERSION_LABEL = "versionLabel"; + public static final String PROP_CREATED_DATE = ContentModel.PROP_CREATED.getLocalName(); + public static final String PROP_CREATOR = ContentModel.PROP_CREATOR.getLocalName(); + public static final String PROP_VERSION_TYPE = "versionType"; + public static final String PROP_VERSION_NUMBER = "versionNumber"; + public static final String PROP_FROZEN_NODE_ID = "frozenNodeId"; + public static final String PROP_FROZEN_NODE_TYPE = "frozenNodeType"; + public static final String PROP_FROZEN_NODE_STORE_PROTOCOL = "frozenNodeStoreProtocol"; + public static final String PROP_FROZEN_NODE_STORE_ID = "frozenNodeStoreId"; + public static final String PROP_FROZEN_ASPECTS = "frozenAspects"; + + /** + * Version history type + */ + public static final String TYPE_VERSION_HISTORY = "versionHistory"; + public static final QName TYPE_QNAME_VERSION_HISTORY = QName.createQName(NAMESPACE_URI, TYPE_VERSION_HISTORY); + + /** + * Version history properties and associations + */ + public static final String PROP_VERSIONED_NODE_ID = "versionedNodeId"; + public static final QName PROP_QNAME_VERSIONED_NODE_ID = QName.createQName(NAMESPACE_URI, PROP_VERSIONED_NODE_ID); + public static final QName ASSOC_ROOT_VERSION = QName.createQName(NAMESPACE_URI, "rootVersion"); + + /** + * Verison type + */ + public static final String TYPE_VERSION = "version"; + public static final QName TYPE_QNAME_VERSION = QName.createQName(NAMESPACE_URI, TYPE_VERSION); + + /** + * Version type properties and associations + */ + public static final QName PROP_QNAME_VERSION_LABEL = QName.createQName(NAMESPACE_URI, PROP_VERSION_LABEL); + public static final QName PROP_QNAME_VERSION_NUMBER = QName.createQName(NAMESPACE_URI, PROP_VERSION_NUMBER); + public static final QName PROP_QNAME_FROZEN_NODE_ID = QName.createQName(NAMESPACE_URI, PROP_FROZEN_NODE_ID); + public static final QName PROP_QNAME_FROZEN_NODE_TYPE = QName.createQName(NAMESPACE_URI, PROP_FROZEN_NODE_TYPE); + public static final QName PROP_QNAME_FROZEN_NODE_STORE_PROTOCOL = QName.createQName(NAMESPACE_URI, PROP_FROZEN_NODE_STORE_PROTOCOL); + public static final QName PROP_QNAME_FROZEN_NODE_STORE_ID = QName.createQName(NAMESPACE_URI, PROP_FROZEN_NODE_STORE_ID); + public static final QName PROP_QNAME_FROZEN_ASPECTS = QName.createQName(NAMESPACE_URI, PROP_FROZEN_ASPECTS); + public static final QName ASSOC_SUCCESSOR = QName.createQName(NAMESPACE_URI, "successor"); + + /** + * Version Meta Data Value type + */ + public static final String TYPE_VERSION_META_DATA_VALUE = "versionMetaDataValue"; + public static final QName TYPE_QNAME_VERSION_META_DATA_VALUE = QName.createQName(NAMESPACE_URI, TYPE_VERSION_META_DATA_VALUE); + + /** + * Version Meta Data Value attributes + */ + public static final String PROP_META_DATA_NAME = "metaDataName"; + public static final QName PROP_QNAME_META_DATA_NAME = QName.createQName(NAMESPACE_URI, PROP_META_DATA_NAME); + public static final String PROP_META_DATA_VALUE = "metaDataValue"; + public static final QName PROP_QNAME_META_DATA_VALUE = QName.createQName(NAMESPACE_URI, PROP_META_DATA_VALUE); + + /** + * Versioned attribute type + */ + public static final String TYPE_VERSIONED_PROPERTY = "versionedProperty"; + public static final QName TYPE_QNAME_VERSIONED_PROPERTY = QName.createQName(NAMESPACE_URI, TYPE_VERSIONED_PROPERTY); + + /** + * Versioned attribute properties + */ + public static final String PROP_QNAME = "qname"; + public static final String PROP_VALUE = "value"; + public static final String PROP_MULTI_VALUE = "multiValue"; + public static final String PROP_IS_MULTI_VALUE = "isMultiValue"; + public static final QName PROP_QNAME_QNAME = QName.createQName(NAMESPACE_URI, PROP_QNAME); + public static final QName PROP_QNAME_VALUE = QName.createQName(NAMESPACE_URI, PROP_VALUE); + public static final QName PROP_QNAME_MULTI_VALUE = QName.createQName(NAMESPACE_URI, PROP_MULTI_VALUE); + public static final QName PROP_QNAME_IS_MULTI_VALUE = QName.createQName(NAMESPACE_URI, PROP_IS_MULTI_VALUE); + + /** + * Versioned child assoc type + */ + public static final String TYPE_VERSIONED_CHILD_ASSOC = "versionedChildAssoc"; + public static final QName TYPE_QNAME_VERSIONED_CHILD_ASSOC = QName.createQName(NAMESPACE_URI, TYPE_VERSIONED_CHILD_ASSOC); + + /** + * Versioned child assoc properties + */ + public static final String PROP_ASSOC_QNAME = "assocQName"; + public static final String PROP_ASSOC_TYPE_QNAME = "assocTypeQName"; + public static final String PROP_IS_PRIMARY = "isPrimary"; + public static final String PROP_NTH_SIBLING = "nthSibling"; + public static final QName PROP_QNAME_ASSOC_QNAME = QName.createQName(NAMESPACE_URI, PROP_ASSOC_QNAME); + public static final QName PROP_QNAME_ASSOC_TYPE_QNAME = QName.createQName(NAMESPACE_URI, PROP_ASSOC_TYPE_QNAME); + public static final QName PROP_QNAME_IS_PRIMARY = QName.createQName(NAMESPACE_URI, PROP_IS_PRIMARY); + public static final QName PROP_QNAME_NTH_SIBLING = QName.createQName(NAMESPACE_URI, PROP_NTH_SIBLING); + + /** + * Versioned assoc type + */ + public static final String TYPE_VERSIONED_ASSOC = "versionedAssoc"; + public static final QName TYPE_QNAME_VERSIONED_ASSOC = QName.createQName(NAMESPACE_URI, TYPE_VERSIONED_ASSOC); + + /** + * Child relationship names + */ + public static final String CHILD_VERSION_HISTORIES = "versionHistory"; + public static final String CHILD_VERSIONS = "version"; + public static final String CHILD_VERSIONED_ATTRIBUTES = "versionedAttributes"; + public static final String CHILD_VERSIONED_CHILD_ASSOCS = "versionedChildAssocs"; + public static final String CHILD_VERSIONED_ASSOCS = "versionedAssocs"; + public static final String CHILD_VERSION_META_DATA = "versionMetaData"; + + public static final QName CHILD_QNAME_VERSION_HISTORIES = QName.createQName(NAMESPACE_URI, CHILD_VERSION_HISTORIES); + public static final QName CHILD_QNAME_VERSIONS = QName.createQName(NAMESPACE_URI, CHILD_VERSIONS); + public static final QName CHILD_QNAME_VERSIONED_ATTRIBUTES = QName.createQName(NAMESPACE_URI, CHILD_VERSIONED_ATTRIBUTES); + public static final QName CHILD_QNAME_VERSIONED_CHILD_ASSOCS = QName.createQName(NAMESPACE_URI, CHILD_VERSIONED_CHILD_ASSOCS); + public static final QName CHILD_QNAME_VERSIONED_ASSOCS = QName.createQName(NAMESPACE_URI, CHILD_VERSIONED_ASSOCS); + public static final QName CHILD_QNAME_VERSION_META_DATA = QName.createQName(NAMESPACE_URI, CHILD_VERSION_META_DATA); +} diff --git a/source/java/org/alfresco/repo/version/VersionServiceImpl.java b/source/java/org/alfresco/repo/version/VersionServiceImpl.java new file mode 100644 index 0000000000..ec788fc89c --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionServiceImpl.java @@ -0,0 +1,1119 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.repo.version.common.AbstractVersionServiceImpl; +import org.alfresco.repo.version.common.VersionHistoryImpl; +import org.alfresco.repo.version.common.VersionImpl; +import org.alfresco.repo.version.common.VersionUtil; +import org.alfresco.repo.version.common.counter.VersionCounterDaoService; +import org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicy; +import org.alfresco.service.cmr.repository.AspectMissingException; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.version.ReservedVersionNameException; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionHistory; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.version.VersionServiceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.ParameterCheck; + +/** + * The version service implementation. + * + * @author Roy Wetheral + */ +public class VersionServiceImpl extends AbstractVersionServiceImpl + implements VersionService, VersionModel +{ + /** + * Error message I18N id's + */ + private static final String MSGID_ERR_NOT_FOUND = "version_service.err_not_found"; + private static final String MSGID_ERR_NO_BRANCHES = "version_service.err_unsupported"; + private static final String MSGID_ERR_RESTORE_EXISTS = "version_service.err_restore_exists"; + private static final String MSGID_ERR_ONE_PRECEEDING = "version_service.err_one_preceeding"; + private static final String MSGID_ERR_RESTORE_NO_VERSION = "version_service.err_restore_no_version"; + private static final String MSGID_ERR_REVERT_MISMATCH = "version_service.err_revert_mismatch"; + + /** + * The version counter service + */ + private VersionCounterDaoService versionCounterService ; + + /** + * The db node service, used as the version store implementation + */ + protected NodeService dbNodeService; + + /** + * Policy behaviour filter + */ + private BehaviourFilter policyBehaviourFilter; + + /** + * The repository searcher + */ + @SuppressWarnings("unused") + private SearchService searcher; + + /** + * The version cache + */ + private HashMap versionCache = new HashMap(100); + + /** + * Sets the db node service, used as the version store implementation + * + * @param nodeService the node service + */ + public void setDbNodeService(NodeService nodeService) + { + this.dbNodeService = nodeService; + } + + /** + * Sets the searcher + * + * @param searcher the searcher + */ + public void setSearcher(SearchService searcher) + { + this.searcher = searcher; + } + + /** + * Sets the version counter service + * + * @param versionCounterService the version counter service + */ + public void setVersionCounterDaoService(VersionCounterDaoService versionCounterService) + { + this.versionCounterService = versionCounterService; + } + + /** + * Set the policy behaviour filter + * + * @param policyBehaviourFilter the policy behaviour filter + */ + public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter) + { + this.policyBehaviourFilter = policyBehaviourFilter; + } + + /** + * Initialise method + */ + @Override + public void initialise() + { + super.initialise(); + + // Register the serial version label behaviour + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "calculateVersionLabel"), + ContentModel.TYPE_CMOBJECT, + new JavaBehaviour(new SerialVersionLabelPolicy(), "calculateVersionLabel")); + } + + /** + * Gets the reference to the version store + * + * @return reference to the version store + */ + public StoreRef getVersionStoreReference() + { + return new StoreRef( + StoreRef.PROTOCOL_WORKSPACE, + VersionModel.STORE_ID); + } + + /** + * @see VersionCounterDaoService#nextVersionNumber(StoreRef) + */ + public Version createVersion( + NodeRef nodeRef, + Map versionProperties) + throws ReservedVersionNameException, AspectMissingException + { + // Get the next version number + int versionNumber = this.versionCounterService.nextVersionNumber(getVersionStoreReference()); + + // Create the version + return createVersion(nodeRef, versionProperties, versionNumber); + } + + /** + * The version's are created from the children upwards with the parent being created first. This will + * ensure that the child version references in the version node will point to the version history nodes + * for the (possibly) newly created version histories. + */ + public Collection createVersion( + NodeRef nodeRef, + Map versionProperties, + boolean versionChildren) + throws ReservedVersionNameException, AspectMissingException + { + // Get the next version number + int versionNumber = this.versionCounterService.nextVersionNumber(getVersionStoreReference()); + + // Create the versions + return createVersion(nodeRef, versionProperties, versionChildren, versionNumber); + } + + /** + * Helper method used to create the version when the versionChildren flag is provided. This method + * ensures that all the children (if the falg is set to true) are created with the same version + * number, this ensuring that the version stripe is correct. + * + * @param nodeRef the parent node reference + * @param versionProperties the version properties + * @param versionChildren indicates whether to version the children of the parent + * node + * @param versionNumber the version number + + * @return a collection of the created versions + * @throws ReservedVersionNameException thrown if there is a reserved version property name clash + * @throws AspectMissingException thrown if the version aspect is missing from a node + */ + private Collection createVersion( + NodeRef nodeRef, + Map versionProperties, + boolean versionChildren, + int versionNumber) + throws ReservedVersionNameException, AspectMissingException + { + + Collection result = new ArrayList(); + + if (versionChildren == true) + { + // Get the children of the node + Collection children = this.dbNodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef childAssoc : children) + { + // Recurse into this method to version all the children with the same version number + Collection childVersions = createVersion( + childAssoc.getChildRef(), + versionProperties, + versionChildren, + versionNumber); + result.addAll(childVersions); + } + } + + result.add(createVersion(nodeRef, versionProperties, versionNumber)); + + return result; + } + + /** + * Note: we can't control the order of the list, so if we have children and parents in the list and the + * parents get versioned before the children and the children are not already versioned then the parents + * child references will be pointing to the node ref, rather than the verison history. + */ + public Collection createVersion( + Collection nodeRefs, + Map versionProperties) + throws ReservedVersionNameException, AspectMissingException + { + Collection result = new ArrayList(nodeRefs.size()); + + // Get the next version number + int versionNumber = this.versionCounterService.nextVersionNumber(getVersionStoreReference()); + + // Version each node in the list + for (NodeRef nodeRef : nodeRefs) + { + result.add(createVersion(nodeRef, versionProperties, versionNumber)); + } + + return result; + } + + /** + * Creates a new version of the passed node assigning the version properties + * accordingly. + * + * @param nodeRef a node reference + * @param versionProperties the version properties + * @param versionNumber the version number + * @return the newly created version + * @throws ReservedVersionNameException + * thrown if there is a name clash in the version properties + */ + private Version createVersion( + NodeRef nodeRef, + Map origVersionProperties, + int versionNumber) + throws ReservedVersionNameException + { + + // Copy the version properties (to prevent unexpected side effects to the caller) + Map versionProperties = new HashMap(); + if (origVersionProperties != null) + { + versionProperties.putAll(origVersionProperties); + } + + // If the version aspect is not there then add it + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == false) + { + this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + } + + // Call the policy behaviour + invokeBeforeCreateVersion(nodeRef); + + // Check that the supplied additional version properties do not clash with the reserved ones + VersionUtil.checkVersionPropertyNames(versionProperties.keySet()); + + // Check the repository for the version history for this node + NodeRef versionHistoryRef = getVersionHistoryNodeRef(nodeRef); + NodeRef currentVersionRef = null; + + if (versionHistoryRef == null) + { + HashMap props = new HashMap(); + props.put(PROP_QNAME_VERSIONED_NODE_ID, nodeRef.getId()); + + // Create a new version history node + ChildAssociationRef childAssocRef = this.dbNodeService.createNode( + getRootNode(), + ContentModel.ASSOC_CHILDREN, + CHILD_QNAME_VERSION_HISTORIES, + TYPE_QNAME_VERSION_HISTORY, + props); + versionHistoryRef = childAssocRef.getChildRef(); + } + else + { + // Since we have an exisiting version history we should be able to lookup + // the current version + currentVersionRef = getCurrentVersionNodeRef(versionHistoryRef, nodeRef); + + if (currentVersionRef == null) + { + throw new VersionServiceException(MSGID_ERR_NOT_FOUND); + } + + // Need to check that we are not about to create branch since this is not currently supported + VersionHistory versionHistory = buildVersionHistory(versionHistoryRef, nodeRef); + Version currentVersion = getVersion(currentVersionRef); + if (versionHistory.getSuccessors(currentVersion).size() != 0) + { + throw new VersionServiceException(MSGID_ERR_NO_BRANCHES); + } + } + + // Create the node details + QName classRef = this.nodeService.getType(nodeRef); + PolicyScope nodeDetails = new PolicyScope(classRef); + + // Get the node details by calling the onVersionCreate policy behaviour + invokeOnCreateVersion(nodeRef, versionProperties, nodeDetails); + + // Create the new version node (child of the version history) + NodeRef newVersionRef = createNewVersion( + nodeRef, + versionHistoryRef, + getStandardVersionProperties(versionProperties, nodeRef, currentVersionRef, versionNumber), + versionProperties, + nodeDetails); + + if (currentVersionRef == null) + { + // Set the new version to be the root version in the version history + this.dbNodeService.createAssociation( + versionHistoryRef, + newVersionRef, + VersionServiceImpl.ASSOC_ROOT_VERSION); + } + else + { + // Relate the new version to the current version as its successor + this.dbNodeService.createAssociation( + currentVersionRef, + newVersionRef, + VersionServiceImpl.ASSOC_SUCCESSOR); + } + + // Create the version data object + Version version = getVersion(newVersionRef); + + // Set the new version label on the versioned node + this.nodeService.setProperty( + nodeRef, + ContentModel.PROP_VERSION_LABEL, + version.getVersionLabel()); + + // Return the data object representing the newly created version + return version; + } + + /** + * @see org.alfresco.service.cmr.version.VersionService#getVersionHistory(NodeRef) + */ + public VersionHistory getVersionHistory(NodeRef nodeRef) + { + VersionHistory versionHistory = null; + + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == true) + { + NodeRef versionHistoryRef = getVersionHistoryNodeRef(nodeRef); + if (versionHistoryRef != null) + { + versionHistory = buildVersionHistory(versionHistoryRef, nodeRef); + } + } + + return versionHistory; + } + + /** + * @see VersionService#getCurrentVersion(NodeRef) + */ + public Version getCurrentVersion(NodeRef nodeRef) + { + Version version = null; + + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == true) + { + VersionHistory versionHistory = getVersionHistory(nodeRef); + if (versionHistory != null) + { + String versionLabel = (String)this.nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL); + version = versionHistory.getVersion(versionLabel); + } + } + + return version; + } + + /** + * Get a map containing the standard list of version properties populated. + * + * @param versionProperties the version meta data properties + * @param nodeRef the node reference + * @param preceedingNodeRef the preceeding node reference + * @param versionNumber the version number + * @return the standard version properties + */ + private Map getStandardVersionProperties(Map versionProperties, NodeRef nodeRef, NodeRef preceedingNodeRef, int versionNumber) + { + Map result = new HashMap(10); + + // Set the version number for the new version + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_VERSION_NUMBER), Integer.toString(versionNumber)); + + // Set the versionable node id + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_NODE_ID), nodeRef.getId()); + + // Set the versionable node store protocol + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_NODE_STORE_PROTOCOL), nodeRef.getStoreRef().getProtocol()); + + // Set the versionable node store id + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_NODE_STORE_ID), nodeRef.getStoreRef().getIdentifier()); + + // Store the current node type + QName nodeType = this.nodeService.getType(nodeRef); + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_NODE_TYPE), nodeType); + + // Store the current aspects + Set aspects = this.nodeService.getAspects(nodeRef); + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_ASPECTS), (Serializable)aspects); + + // Calculate the version label + QName classRef = this.nodeService.getType(nodeRef); + Version preceedingVersion = getVersion(preceedingNodeRef); + String versionLabel = invokeCalculateVersionLabel(classRef, preceedingVersion, versionNumber, versionProperties); + result.put(QName.createQName(NAMESPACE_URI, VersionModel.PROP_VERSION_LABEL), versionLabel); + + return result; + } + + /** + * Creates a new version node, setting the properties both calculated and specified. + * + * @param versionableNodeRef the reference to the node being versioned + * @param versionHistoryRef version history node reference + * @param preceedingNodeRef the version node preceeding this in the version history + * , null if none + * @param versionProperties version properties + * @param versionNumber the version number + * @return the version node reference + */ + private NodeRef createNewVersion( + NodeRef versionableNodeRef, + NodeRef versionHistoryRef, + Map standardVersionProperties, + Map versionProperties, + PolicyScope nodeDetails) + { + // Create the new version + ChildAssociationRef childAssocRef = this.dbNodeService.createNode( + versionHistoryRef, + CHILD_QNAME_VERSIONS, + CHILD_QNAME_VERSIONS, + TYPE_QNAME_VERSION, + standardVersionProperties); + NodeRef versionNodeRef = childAssocRef.getChildRef(); + + // Store the meta data + storeVersionMetaData(versionNodeRef, versionProperties); + + // Freeze the various parts of the node + freezeProperties(versionNodeRef, nodeDetails.getProperties()); + freezeChildAssociations(versionNodeRef, nodeDetails.getChildAssociations()); + freezeAssociations(versionNodeRef, nodeDetails.getAssociations()); + freezeAspects(nodeDetails, versionNodeRef, nodeDetails.getAspects()); + + // Return the created node reference + return versionNodeRef; + } + + /** + * Store the version meta data + * + * @param versionNodeRef the version node reference + * @param versionProperties the version properties + */ + private void storeVersionMetaData(NodeRef versionNodeRef, Map versionProperties) + { + for (Map.Entry entry : versionProperties.entrySet()) + { + HashMap properties = new HashMap(); + + properties.put(PROP_QNAME_META_DATA_NAME, entry.getKey()); + properties.put(PROP_QNAME_META_DATA_VALUE, entry.getValue()); + + this.dbNodeService.createNode( + versionNodeRef, + CHILD_QNAME_VERSION_META_DATA, + CHILD_QNAME_VERSION_META_DATA, + TYPE_QNAME_VERSION_META_DATA_VALUE, + properties); + } + } + + /** + * Freeze the aspects + * + * @param nodeDetails the node details + * @param versionNodeRef the version node reference + * @param aspects the set of aspects + */ + private void freezeAspects(PolicyScope nodeDetails, NodeRef versionNodeRef, Set aspects) + { + for (QName aspect : aspects) + { + // Freeze the details of the aspect + freezeProperties(versionNodeRef, nodeDetails.getProperties(aspect)); + freezeChildAssociations(versionNodeRef, nodeDetails.getChildAssociations(aspect)); + freezeAssociations(versionNodeRef, nodeDetails.getAssociations(aspect)); + } + } + + /** + * Freeze associations + * + * @param versionNodeRef the version node reference + * @param associations the list of associations + */ + private void freezeAssociations(NodeRef versionNodeRef, List associations) + { + for (AssociationRef targetAssoc : associations) + { + HashMap properties = new HashMap(); + + // Set the qname of the association + properties.put(PROP_QNAME_ASSOC_TYPE_QNAME, targetAssoc.getTypeQName()); + + // Set the reference property to point to the child node + properties.put(ContentModel.PROP_REFERENCE, targetAssoc.getTargetRef()); + + // Create child version reference + this.dbNodeService.createNode( + versionNodeRef, + CHILD_QNAME_VERSIONED_ASSOCS, + CHILD_QNAME_VERSIONED_ASSOCS, + TYPE_QNAME_VERSIONED_ASSOC, + properties); + } + } + + /** + * Freeze child associations + * + * @param versionNodeRef the version node reference + * @param childAssociations the child associations + */ + private void freezeChildAssociations(NodeRef versionNodeRef, List childAssociations) + { + for (ChildAssociationRef childAssocRef : childAssociations) + { + HashMap properties = new HashMap(); + + // Set the qname, isPrimary and nthSibling properties + properties.put(PROP_QNAME_ASSOC_QNAME, childAssocRef.getQName()); + properties.put(PROP_QNAME_ASSOC_TYPE_QNAME, childAssocRef.getTypeQName()); + properties.put(PROP_QNAME_IS_PRIMARY, Boolean.valueOf(childAssocRef.isPrimary())); + properties.put(PROP_QNAME_NTH_SIBLING, Integer.valueOf(childAssocRef.getNthSibling())); + + // Set the reference property to point to the child node + properties.put(ContentModel.PROP_REFERENCE, childAssocRef.getChildRef()); + + // Create child version reference + this.dbNodeService.createNode( + versionNodeRef, + CHILD_QNAME_VERSIONED_CHILD_ASSOCS, + CHILD_QNAME_VERSIONED_CHILD_ASSOCS, + TYPE_QNAME_VERSIONED_CHILD_ASSOC, + properties); + } + } + + /** + * Freeze properties + * + * @param versionNodeRef the version node reference + * @param properties the properties + */ + private void freezeProperties(NodeRef versionNodeRef, Map properties) + { + // Copy the property values from the node onto the version node + for (Map.Entry entry : properties.entrySet()) + { + // Get the property values + HashMap props = new HashMap(); + props.put(PROP_QNAME_QNAME, entry.getKey()); + + if (entry.getValue() instanceof Collection) + { + props.put(PROP_QNAME_MULTI_VALUE, entry.getValue()); + props.put(PROP_QNAME_IS_MULTI_VALUE, true); + } + else + { + props.put(PROP_QNAME_VALUE, entry.getValue()); + props.put(PROP_QNAME_IS_MULTI_VALUE, false); + } + + // Create the node storing the frozen attribute details + this.dbNodeService.createNode( + versionNodeRef, + CHILD_QNAME_VERSIONED_ATTRIBUTES, + CHILD_QNAME_VERSIONED_ATTRIBUTES, + TYPE_QNAME_VERSIONED_PROPERTY, + props); + } + } + + /** + * Gets the version stores root node + * + * @return the node ref to the root node of the version store + */ + private NodeRef getRootNode() + { + // Get the version store root node reference + return this.dbNodeService.getRootNode(getVersionStoreReference()); + } + + /** + * Builds a version history object from the version history reference. + *

    + * The node ref is passed to enable the version history to be scoped to the + * appropriate branch in the version history. + * + * @param versionHistoryRef the node ref for the version history + * @param nodeRef the node reference + * @return a constructed version history object + */ + private VersionHistory buildVersionHistory(NodeRef versionHistoryRef, NodeRef nodeRef) + { + VersionHistory versionHistory = null; + + ArrayList versionHistoryNodeRefs = new ArrayList(); + NodeRef currentVersion = getCurrentVersionNodeRef(versionHistoryRef, nodeRef); + + while (currentVersion != null) + { + AssociationRef preceedingVersion = null; + + versionHistoryNodeRefs.add(0, currentVersion); + + List preceedingVersions = this.dbNodeService.getSourceAssocs( + currentVersion, + VersionModel.ASSOC_SUCCESSOR); + if (preceedingVersions.size() == 1) + { + preceedingVersion = (AssociationRef)preceedingVersions.toArray()[0]; + currentVersion = preceedingVersion.getSourceRef(); + } + else if (preceedingVersions.size() > 1) + { + // Error since we only currently support one preceeding version + throw new VersionServiceException(MSGID_ERR_ONE_PRECEEDING); + } + else + { + currentVersion = null; + } + } + + // Build the version history object + boolean isRoot = true; + Version preceeding = null; + for (NodeRef versionRef : versionHistoryNodeRefs) + { + Version version = getVersion(versionRef); + + if (isRoot == true) + { + versionHistory = new VersionHistoryImpl(version); + isRoot = false; + } + else + { + ((VersionHistoryImpl)versionHistory).addVersion(version, preceeding); + } + preceeding = version; + } + + return versionHistory; + } + + /** + * Constructs the a version object to contain the version information from the version node ref. + * + * @param versionRef the version reference + * @return object containing verison data + */ + private Version getVersion(NodeRef versionRef) + { + Version result = null; + + if (versionRef != null) + { + // check to see if this version is already in the cache + result = this.versionCache.get(versionRef); + + if (result == null) + { + Map versionProperties = new HashMap(); + + // Get the standard node details + Map nodeProperties = this.dbNodeService.getProperties(versionRef); + for (QName key : nodeProperties.keySet()) + { + Serializable value = nodeProperties.get(key); + versionProperties.put(key.getLocalName(), value); + } + + // Get the meta data + List metaData = + this.dbNodeService.getChildAssocs(versionRef, RegexQNamePattern.MATCH_ALL, CHILD_QNAME_VERSION_META_DATA); + for (ChildAssociationRef ref : metaData) + { + NodeRef metaDataValue = (NodeRef)ref.getChildRef(); + String name = (String)this.dbNodeService.getProperty(metaDataValue, PROP_QNAME_META_DATA_NAME); + Serializable value = this.dbNodeService.getProperty(metaDataValue, PROP_QNAME_META_DATA_VALUE); + versionProperties.put(name, value); + } + + // Create and return the version object + NodeRef newNodeRef = new NodeRef(new StoreRef(STORE_PROTOCOL, STORE_ID), versionRef.getId()); + result = new VersionImpl(versionProperties, newNodeRef); + + // Add the version to the cache + this.versionCache.put(versionRef, result); + } + } + + return result; + } + + /** + * Gets a reference to the version history node for a given 'real' node. + * + * @param nodeRef a node reference + * @return a reference to the version history node, null of none + */ + private NodeRef getVersionHistoryNodeRef(NodeRef nodeRef) + { + NodeRef result = null; + + Collection versionHistories = this.dbNodeService.getChildAssocs(getRootNode()); + for (ChildAssociationRef versionHistory : versionHistories) + { + String nodeId = (String)this.dbNodeService.getProperty(versionHistory.getChildRef(), VersionModel.PROP_QNAME_VERSIONED_NODE_ID); + if (nodeId != null && nodeId.equals(nodeRef.getId()) == true) + { + result = versionHistory.getChildRef(); + break; + } + } + + return result; + } + + /** + * Gets a reference to the node for the current version of the passed node ref. + * + * This uses the version label as a mechanism for looking up the version node in + * the version history. + * + * @param nodeRef a node reference + * @return a reference to a version reference + */ + private NodeRef getCurrentVersionNodeRef(NodeRef versionHistory, NodeRef nodeRef) + { + NodeRef result = null; + String versionLabel = (String)this.nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL); + + Collection versions = this.dbNodeService.getChildAssocs(versionHistory); + for (ChildAssociationRef version : versions) + { + String tempLabel = (String)this.dbNodeService.getProperty(version.getChildRef(), VersionModel.PROP_QNAME_VERSION_LABEL); + if (tempLabel != null && tempLabel.equals(versionLabel) == true) + { + result = version.getChildRef(); + break; + } + } + + return result; + } + + /** + * Checks the given node for the version aspect. Throws an exception if it is not present. + * + * @param nodeRef the node reference + * @throws AspectMissingException + * the version aspect is not present on the node + */ + private void checkForVersionAspect(NodeRef nodeRef) + throws AspectMissingException + { + QName aspectRef = ContentModel.ASPECT_VERSIONABLE; + + if (this.nodeService.hasAspect(nodeRef, aspectRef) == false) + { + // Raise exception to indicate version aspect is not present + throw new AspectMissingException(aspectRef, nodeRef); + } + } + + /** + * @see org.alfresco.cms.version.VersionService#revert(NodeRef) + */ + public void revert(NodeRef nodeRef) + { + revert(nodeRef, getCurrentVersion(nodeRef), true); + } + + /** + * @see org.alfresco.service.cmr.version.VersionService#revert(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + public void revert(NodeRef nodeRef, boolean deep) + { + revert(nodeRef, getCurrentVersion(nodeRef), deep); + } + + /** + * @see org.alfresco.service.cmr.version.VersionService#revert(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.version.Version) + */ + public void revert(NodeRef nodeRef, Version version) + { + revert(nodeRef, version, true); + } + + /** + * @see org.alfresco.service.cmr.version.VersionService#revert(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.version.Version, boolean) + */ + public void revert(NodeRef nodeRef, Version version, boolean deep) + { + // Check the mandatory parameters + ParameterCheck.mandatory("nodeRef", nodeRef); + ParameterCheck.mandatory("version", version); + + // Cross check that the version provided relates to the node reference provided + if (nodeRef.getId().equals(version.getVersionProperty(VersionModel.PROP_FROZEN_NODE_ID)) == false) + { + // Error since the version provided does not correspond to the node reference provided + throw new VersionServiceException(MSGID_ERR_REVERT_MISMATCH); + } + + // Turn off any auto-version policy behaviours + this.policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + try + { + // Store the current version label + String currentVersionLabel = (String)this.nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL); + + // Get the node that represents the frozen state + NodeRef versionNodeRef = version.getFrozenStateNodeRef(); + + // Revert the property values + this.nodeService.setProperties(nodeRef, this.nodeService.getProperties(versionNodeRef)); + + // Apply/remove the aspects as required + Set aspects = new HashSet(this.nodeService.getAspects(nodeRef)); + for (QName versionAspect : this.nodeService.getAspects(versionNodeRef)) + { + if (aspects.contains(versionAspect) == false) + { + this.nodeService.addAspect(nodeRef, versionAspect, null); + } + else + { + aspects.remove(versionAspect); + } + } + for (QName aspect : aspects) + { + this.nodeService.removeAspect(nodeRef, aspect); + } + + // Re-add the versionable aspect to the reverted node + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == false) + { + this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + } + + // Re-set the version label property (since it should not be modified from the origional) + this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, currentVersionLabel); + + // Add/remove the child nodes + List children = new ArrayList(this.nodeService.getChildAssocs(nodeRef)); + for (ChildAssociationRef versionedChild : this.nodeService.getChildAssocs(versionNodeRef)) + { + if (children.contains(versionedChild) == false) + { + if (this.nodeService.exists(versionedChild.getChildRef()) == true) + { + // The node was a primary child of the parent, but that is no longer the case. Dispite this + // the node still exits so this means it has been moved. + // The best thing to do in this situation will be to re-add the node as a child, but it will not + // be a primary child. + this.nodeService.addChild(nodeRef, versionedChild.getChildRef(), versionedChild.getTypeQName(), versionedChild.getQName()); + } + else + { + if (versionedChild.isPrimary() == true) + { + // Only try to resotre missing children if we are doing a deep revert + // Look and see if we have a version history for the child node + if (deep == true && getVersionHistoryNodeRef(versionedChild.getChildRef()) != null) + { + // We're going to try and restore the missing child node and recreate the assoc + restore( + versionedChild.getChildRef(), + nodeRef, + versionedChild.getTypeQName(), + versionedChild.getQName()); + } + // else the deleted child did not have a version history so we can't restore the child + // and so we can't revert the association + } + + // else + // Since this was never a primary assoc and the child has been deleted we won't recreate + // the missing node as it was never owned by the node and we wouldn't know where to put it. + } + } + else + { + children.remove(versionedChild); + } + } + for (ChildAssociationRef ref : children) + { + this.nodeService.removeChild(nodeRef, ref.getChildRef()); + } + + // Add/remove the target associations + for (AssociationRef assocRef : this.nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL)) + { + this.nodeService.removeAssociation(assocRef.getSourceRef(), assocRef.getTargetRef(), assocRef.getTypeQName()); + } + for (AssociationRef versionedAssoc : this.nodeService.getTargetAssocs(versionNodeRef, RegexQNamePattern.MATCH_ALL)) + { + if (this.nodeService.exists(versionedAssoc.getTargetRef()) == true) + { + this.nodeService.createAssociation(nodeRef, versionedAssoc.getTargetRef(), versionedAssoc.getTypeQName()); + } + + // else + // Since the tareget of the assoc no longer exists we can't recreate the assoc + } + } + finally + { + // Turn auto-version policies back on + this.policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + } + } + + /** + * @see org.alfresco.service.cmr.version.VersionService#restore(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName) + */ + public NodeRef restore( + NodeRef nodeRef, + NodeRef parentNodeRef, + QName assocTypeQName, + QName assocQName) + { + return restore(nodeRef, parentNodeRef, assocTypeQName, assocQName, true); + } + + /** + * @see org.alfresco.service.cmr.version.VersionService#restore(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, boolean) + */ + public NodeRef restore( + NodeRef nodeRef, + NodeRef parentNodeRef, + QName assocTypeQName, + QName assocQName, + boolean deep) + { + NodeRef restoredNodeRef = null; + + // Check that the node does not exist + if (this.nodeService.exists(nodeRef) == true) + { + // Error since you can not restore a node that already exists + throw new VersionServiceException(MSGID_ERR_RESTORE_EXISTS, new Object[]{nodeRef.toString()}); + } + + // Try and get the version details that we want to restore to + Version version = getHeadVersion(nodeRef); + if (version == null) + { + // Error since there is no version information available to restore the node from + throw new VersionServiceException(MSGID_ERR_RESTORE_NO_VERSION, new Object[]{nodeRef.toString()}); + } + + // Set the uuid of the new node + Map props = new HashMap(1); + props.put(ContentModel.PROP_NODE_UUID, version.getVersionProperty(VersionModel.PROP_FROZEN_NODE_ID)); + + // Get the type of the node node + QName type = (QName)version.getVersionProperty(VersionModel.PROP_FROZEN_NODE_TYPE); + + // Disable auto-version behaviour + this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE); + try + { + // Create the restored node + restoredNodeRef = this.nodeService.createNode( + parentNodeRef, + assocTypeQName, + assocQName, + type, + props).getChildRef(); + } + finally + { + // Enable auto-version behaviour + this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE); + } + + // Now we need to revert the newly restored node + revert(restoredNodeRef, version, deep); + + return restoredNodeRef; + } + + /** + * Get the head version given a node reference + * + * @param nodeRef the node reference + * @return the 'head' version + */ + private Version getHeadVersion(NodeRef nodeRef) + { + Version version = null; + StoreRef storeRef = nodeRef.getStoreRef(); + + NodeRef versionHistoryNodeRef = getVersionHistoryNodeRef(nodeRef); + if (versionHistoryNodeRef != null) + { + List versionsAssoc = this.dbNodeService.getChildAssocs(versionHistoryNodeRef, RegexQNamePattern.MATCH_ALL, VersionModel.CHILD_QNAME_VERSIONS); + for (ChildAssociationRef versionAssoc : versionsAssoc) + { + NodeRef versionNodeRef = versionAssoc.getChildRef(); + List successors = this.dbNodeService.getTargetAssocs(versionNodeRef, VersionModel.ASSOC_SUCCESSOR); + if (successors.size() == 0) + { + String storeProtocol = (String)this.dbNodeService.getProperty( + versionNodeRef, + QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_NODE_STORE_PROTOCOL)); + String storeId = (String)this.dbNodeService.getProperty( + versionNodeRef, + QName.createQName(NAMESPACE_URI, VersionModel.PROP_FROZEN_NODE_STORE_ID)); + StoreRef versionStoreRef = new StoreRef(storeProtocol, storeId); + if (storeRef.equals(versionStoreRef) == true) + { + version = getVersion(versionNodeRef); + } + } + } + } + + return version; + } + + /** + * @see org.alfresco.cms.version.VersionService#deleteVersionHistory(NodeRef) + */ + public void deleteVersionHistory(NodeRef nodeRef) + throws AspectMissingException + { + // First check that the versionable aspect is present + checkForVersionAspect(nodeRef); + + // Get the version history node for the node is question and delete it + NodeRef versionHistoryNodeRef = getVersionHistoryNodeRef(nodeRef); + this.dbNodeService.deleteNode(versionHistoryNodeRef); + + // Reset the version label property on the versionable node + this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, null); + } +} diff --git a/source/java/org/alfresco/repo/version/VersionServiceImplTest.java b/source/java/org/alfresco/repo/version/VersionServiceImplTest.java new file mode 100644 index 0000000000..b203751fa5 --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionServiceImplTest.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.util.Collection; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionHistory; +import org.alfresco.service.cmr.version.VersionServiceException; +import org.alfresco.service.namespace.QName; + +/** + * versionService test class. + * + * @author Roy Wetherall + */ +public class VersionServiceImplTest extends BaseVersionStoreTest +{ + private static final String UPDATED_VALUE_1 = "updatedValue1"; + private static final String UPDATED_VALUE_2 = "updatedValue2"; + private static final String UPDATED_VALUE_3 = "updatedValue3"; + private static final String UPDATED_CONTENT_1 = "updatedContent1"; + private static final String UPDATED_CONTENT_2 = "updatedContent2"; + + /** + * Tests the creation of the initial version of a versionable node + */ + public void testCreateIntialVersion() + { + NodeRef versionableNode = createNewVersionableNode(); + createVersion(versionableNode); + } + + /** + * Test creating a version history with many versions from the same workspace + */ + public void testCreateManyVersionsSameWorkspace() + { + NodeRef versionableNode = createNewVersionableNode(); + createVersion(versionableNode); + // TODO mess with some of the properties and stuff as you version + createVersion(versionableNode); + // TODO mess with some of the properties and stuff as you version + createVersion(versionableNode); + } + + // TODO test versioning a non versionable node ie: no version apsect + + // TODO test versioning numberious times with branchs implies by different workspaces + + /** + * Test versioning the children of a verionable node + */ + public void testVersioningChildren() + { + NodeRef versionableNode = createNewVersionableNode(); + + // Snap shot data + int expectedVersionNumber = peekNextVersionNumber(); + String expectedVersionLabel = peekNextVersionLabel(versionableNode, expectedVersionNumber, versionProperties); + long beforeVersionTime = System.currentTimeMillis(); + + // Version the node and its children + Collection versions = this.versionService.createVersion( + versionableNode, + this.versionProperties, + true); + + // Check the returned versions are correct + CheckVersionCollection(expectedVersionNumber, expectedVersionLabel, beforeVersionTime, versions); + + // TODO check the version history is correct + } + + /** + * Test versioning many nodes in one go + */ + public void testVersioningManyNodes() + { + NodeRef versionableNode = createNewVersionableNode(); + + // Snap shot data + int expectedVersionNumber = peekNextVersionNumber(); + String expectedVersionLabel = peekNextVersionLabel(versionableNode, expectedVersionNumber, versionProperties); + long beforeVersionTime = System.currentTimeMillis(); + + // Version the list of nodes created + Collection versions = this.versionService.createVersion( + this.versionableNodes.values(), + this.versionProperties); + + // Check the returned versions are correct + CheckVersionCollection(expectedVersionNumber, expectedVersionLabel, beforeVersionTime, versions); + + // TODO check the version histories + } + + /** + * Helper method to check the validity of the list of newly created versions. + * + * @param expectedVersionNumber the expected version number that all the versions should have + * @param beforeVersionTime the time before the versions where created + * @param versions the collection of version objects + */ + private void CheckVersionCollection(int expectedVersionNumber, String expectedVersionLabel, long beforeVersionTime, Collection versions) + { + for (Version version : versions) + { + // Get the frozen id from the version + String frozenNodeId = (String)version.getVersionProperty(VersionModel.PROP_FROZEN_NODE_ID); + assertNotNull("Unable to retrieve the frozen node id from the created version.", frozenNodeId); + + // Get the origional node ref (based on the forzen node) + NodeRef origionaNodeRef = this.versionableNodes.get(frozenNodeId); + assertNotNull("The versionable node ref that relates to the frozen node id can not be found.", origionaNodeRef); + + // Check the new version + checkNewVersion(beforeVersionTime, expectedVersionNumber, expectedVersionLabel, version, origionaNodeRef); + } + } + + /** + * Tests the version history + */ + public void testNoVersionHistory() + { + NodeRef nodeRef = createNewVersionableNode(); + + VersionHistory vh = this.versionService.getVersionHistory(nodeRef); + assertNull(vh); + } + + /** + * Tests getVersionHistory when all the entries in the version history + * are from the same workspace. + */ + public void testGetVersionHistorySameWorkspace() + { + NodeRef versionableNode = createNewVersionableNode(); + + Version version1 = addToVersionHistory(versionableNode, null); + Version version2 = addToVersionHistory(versionableNode, version1); + Version version3 = addToVersionHistory(versionableNode, version2); + Version version4 = addToVersionHistory(versionableNode, version3); + addToVersionHistory(versionableNode, version4); + } + + /** + * Adds another version to the version history then checks that getVersionHistory is returning + * the correct data. + * + * @param versionableNode the versionable node reference + * @param parentVersion the parent version + */ + private Version addToVersionHistory(NodeRef versionableNode, Version parentVersion) + { + Version createdVersion = createVersion(versionableNode); + + VersionHistory vh = this.versionService.getVersionHistory(versionableNode); + assertNotNull("The version history should not be null since we know we have versioned this node.", vh); + + if (parentVersion == null) + { + // Check the root is the newly created version + Version root = vh.getRootVersion(); + assertNotNull( + "The root version should never be null, since every version history ust have a root version.", + root); + assertEquals(createdVersion.getVersionLabel(), root.getVersionLabel()); + } + + // Get the version from the version history + Version version = vh.getVersion(createdVersion.getVersionLabel()); + assertNotNull(version); + assertEquals(createdVersion.getVersionLabel(), version.getVersionLabel()); + + // Check that the version is a leaf node of the version history (since it is newly created) + Collection suc = vh.getSuccessors(version); + assertNotNull(suc); + assertEquals(0, suc.size()); + + // Check that the predessor is the passed parent version (if root version should be null) + Version pre = vh.getPredecessor(version); + if (parentVersion == null) + { + assertNull(pre); + } + else + { + assertNotNull(pre); + assertEquals(parentVersion.getVersionLabel(), pre.getVersionLabel()); + } + + if (parentVersion != null) + { + // Check that the successors of the parent are the created version + Collection parentSuc = vh.getSuccessors(parentVersion); + assertNotNull(parentSuc); + assertEquals(1, parentSuc.size()); + Version tempVersion = (Version)parentSuc.toArray()[0]; + assertEquals(version.getVersionLabel(), tempVersion.getVersionLabel()); + } + + return createdVersion; + } + + /** + * Test revert + */ + @SuppressWarnings("unused") + public void testRevert() + { + // Create a versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Store the node details for later + Set origAspects = this.dbNodeService.getAspects(versionableNode); + + // Create the initial version + Version version1 = createVersion(versionableNode); + + // Change the property and content values + this.dbNodeService.setProperty(versionableNode, PROP_1, UPDATED_VALUE_1); + this.dbNodeService.setProperty(versionableNode, PROP_2, null); + ContentWriter contentWriter = this.contentService.getWriter(versionableNode, ContentModel.PROP_CONTENT, true); + assertNotNull(contentWriter); + contentWriter.putContent(UPDATED_CONTENT_1); + + // Change the aspects on the node + this.dbNodeService.addAspect(versionableNode, ContentModel.ASPECT_SIMPLE_WORKFLOW, null); + + // Store the node details for later + Set origAspects2 = this.dbNodeService.getAspects(versionableNode); + + // Create a new version + Version version2 = createVersion(versionableNode); + + // Change the property and content values + this.dbNodeService.setProperty(versionableNode, PROP_1, UPDATED_VALUE_2); + this.dbNodeService.setProperty(versionableNode, PROP_2, UPDATED_VALUE_3); + this.dbNodeService.setProperty(versionableNode, PROP_3, null); + ContentWriter contentWriter2 = this.contentService.getWriter(versionableNode, ContentModel.PROP_CONTENT, true); + assertNotNull(contentWriter2); + contentWriter2.putContent(UPDATED_CONTENT_2); + + String versionLabel = (String)this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL); + + // Revert to the previous version + this.versionService.revert(versionableNode); + + // Check that the version label is unchanged + assertEquals(versionLabel, this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL)); + + // Check that the properties have been reverted + assertEquals(UPDATED_VALUE_1, this.dbNodeService.getProperty(versionableNode, PROP_1)); + assertNull(this.dbNodeService.getProperty(versionableNode, PROP_2)); + assertEquals(VALUE_3, this.dbNodeService.getProperty(versionableNode, PROP_3)); + + // Check that the content has been reverted + ContentReader contentReader1 = this.contentService.getReader(versionableNode, ContentModel.PROP_CONTENT); + assertNotNull(contentReader1); + assertEquals(UPDATED_CONTENT_1, contentReader1.getContentString()); + + // Check that the aspects have been reverted correctly + Set aspects1 = this.dbNodeService.getAspects(versionableNode); + assertEquals(aspects1.size(), origAspects2.size()); + + // Revert to the first version + this.versionService.revert(versionableNode, version1); + + // Check that the version label is correct + assertEquals(versionLabel, this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL)); + + // Check that the properties are correct + assertEquals(VALUE_1, this.dbNodeService.getProperty(versionableNode, PROP_1)); + assertEquals(VALUE_2, this.dbNodeService.getProperty(versionableNode, PROP_2)); + assertEquals(VALUE_3, this.dbNodeService.getProperty(versionableNode, PROP_3)); + + // Check that the content is correct + ContentReader contentReader2 = this.contentService.getReader(versionableNode, ContentModel.PROP_CONTENT); + assertNotNull(contentReader2); + assertEquals(TEST_CONTENT, contentReader2.getContentString()); + + // Check that the aspects have been reverted correctly + Set aspects2 = this.dbNodeService.getAspects(versionableNode); + assertEquals(aspects2.size(), origAspects.size()); + + // Check that the version label is still the same + assertEquals(versionLabel, this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL)); + } + + /** + * Test restore + */ + public void testRestore() + { + // Try and restore a node without any version history + try + { + this.versionService.restore( + new NodeRef(this.testStoreRef, "123"), + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}MyVersionableNode")); + fail("An exception should have been raised since this node has no version history."); + } + catch (VersionServiceException exception) + { + // We where expecting this exception + } + + // Create a versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Store the node details for later + Set origAspects = this.dbNodeService.getAspects(versionableNode); + + // Try and restore the node (fail since exist!!) + try + { + this.versionService.restore( + versionableNode, + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}MyVersionableNode")); + fail("An exception should have been raised since this node exists and you can't restore a node that exists."); + } + catch (VersionServiceException exception) + { + // We where expecting this exception + } + + // Version it + this.versionService.createVersion(versionableNode, null); + + // Delete it + this.dbNodeService.deleteNode(versionableNode); + assertFalse(this.dbNodeService.exists(versionableNode)); + + // Try and resotre it + NodeRef restoredNode = this.versionService.restore( + versionableNode, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}MyVersionableNode")); + + assertNotNull(restoredNode); + assertTrue(this.dbNodeService.exists(restoredNode)); + + // Check that the properties are correct + assertEquals(VALUE_1, this.dbNodeService.getProperty(restoredNode, PROP_1)); + assertEquals(VALUE_2, this.dbNodeService.getProperty(restoredNode, PROP_2)); + assertEquals(VALUE_3, this.dbNodeService.getProperty(restoredNode, PROP_3)); + + // Check that the content is correct + ContentReader contentReader2 = this.contentService.getReader(restoredNode, ContentModel.PROP_CONTENT); + assertNotNull(contentReader2); + assertEquals(TEST_CONTENT, contentReader2.getContentString()); + + // Check that the aspects have been reverted correctly + Set aspects2 = this.dbNodeService.getAspects(restoredNode); + assertEquals(aspects2.size(), origAspects.size()); + } + + /** + * Test deleteVersionHistory + */ + public void testDeleteVersionHistory() + { + // Create a versionable node + NodeRef versionableNode = createNewVersionableNode(); + + // Check that there is no version history + VersionHistory versionHistory1 = this.versionService.getVersionHistory(versionableNode); + assertNull(versionHistory1); + + // Create a couple of versions + createVersion(versionableNode); + Version version1 = createVersion(versionableNode); + + // Check that the version label is correct on the versionable node + String versionLabel1 = (String)this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL); + assertNotNull(versionLabel1); + assertEquals(version1.getVersionLabel(), versionLabel1); + + // Check that the version history has been created correctly + VersionHistory versionHistory2 = this.versionService.getVersionHistory(versionableNode); + assertNotNull(versionHistory2); + assertEquals(2, versionHistory2.getAllVersions().size()); + + // Delete the version history + this.versionService.deleteVersionHistory(versionableNode); + + // Check that there is no version history available for the node + VersionHistory versionHistory3 = this.versionService.getVersionHistory(versionableNode); + assertNull(versionHistory3); + + // Check that the current version property on the versionable node is no longer set + String versionLabel2 = (String)this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL); + assertNull(versionLabel2); + + // Create a couple of versions + createVersion(versionableNode); + Version version2 = createVersion(versionableNode); + + // Check that the version history is correct + VersionHistory versionHistory4 = this.versionService.getVersionHistory(versionableNode); + assertNotNull(versionHistory4); + assertEquals(2, versionHistory4.getAllVersions().size()); + + // Check that the version label is correct on the versionable node + String versionLabel3 = (String)this.dbNodeService.getProperty(versionableNode, ContentModel.PROP_VERSION_LABEL); + assertNotNull(versionLabel3); + assertEquals(version2.getVersionLabel(), versionLabel3); + + } + + public void testAutoVersion() + { + // Create a versionable node + final NodeRef versionableNode = createNewVersionableNode(); + + // Add some content + ContentWriter contentWriter = this.contentService.getWriter(versionableNode, ContentModel.PROP_CONTENT, true); + assertNotNull(contentWriter); + contentWriter.putContent(UPDATED_CONTENT_1); + + // Need to commit in order to get the auto version to fire ... + setComplete(); + endTransaction(); + + // Now lets have a look and make sure we have the correct number of entries in the version history + TransactionUtil.executeInUserTransaction(this.transactionService, new TransactionUtil.TransactionWork() + { + public Object doWork() throws Exception + { + VersionHistory versionHistory = VersionServiceImplTest.this.versionService.getVersionHistory(versionableNode); + assertNotNull(versionHistory); + assertEquals(1, versionHistory.getAllVersions().size()); + + return null; + } + + }); + } +} diff --git a/source/java/org/alfresco/repo/version/VersionServicePolicies.java b/source/java/org/alfresco/repo/version/VersionServicePolicies.java new file mode 100644 index 0000000000..b04086c1b3 --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionServicePolicies.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.policy.ClassPolicy; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.namespace.QName; + +/** + * Version service policy interfaces + * + * @author Roy Wetherall + */ +public interface VersionServicePolicies +{ + /** + * Before create version policy interface. + */ + public interface BeforeCreateVersionPolicy extends ClassPolicy + { + /** + * Called before a new version is created for a version + * + * @param versionableNode reference to the node about to be versioned + */ + public void beforeCreateVersion(NodeRef versionableNode); + + } + + /** + * On create version policy interface + */ + public interface OnCreateVersionPolicy extends ClassPolicy + { + public void onCreateVersion( + QName classRef, + NodeRef versionableNode, + Map versionProperties, + PolicyScope nodeDetails); + } + + /** + * Calculate version lable policy interface + */ + public interface CalculateVersionLabelPolicy extends ClassPolicy + { + public String calculateVersionLabel( + QName classRef, + Version preceedingVersion, + int versionNumber, + MapverisonProperties); + } +} diff --git a/source/java/org/alfresco/repo/version/VersionStoreBaseTest_model.xml b/source/java/org/alfresco/repo/version/VersionStoreBaseTest_model.xml new file mode 100644 index 0000000000..1d36ad3cf7 --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionStoreBaseTest_model.xml @@ -0,0 +1,112 @@ + + + VersionStoreBaseTest model + Alfresco + 2005-05-30 + 1.0 + + + + + + + + + + + + + + Test type + The test type + cm:content + + + + d:text + false + + + + d:text + false + + + + d:text + false + + + + d:text + true + + + + + + + false + false + + + test:testtype + false + true + + + + + false + true + + + test:testtype + false + false + + childassoc1 + true + + + + false + true + + + test:testtype + false + false + + childassoc2 + true + + + + + + + + + Test Aspect + The test aspect + + + + + + d:text + false + + + + d:text + false + + + + + + + + diff --git a/source/java/org/alfresco/repo/version/VersionTestSuite.java b/source/java/org/alfresco/repo/version/VersionTestSuite.java new file mode 100644 index 0000000000..dda2207120 --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionTestSuite.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.alfresco.repo.version.common.VersionHistoryImplTest; +import org.alfresco.repo.version.common.VersionImplTest; +import org.alfresco.repo.version.common.counter.VersionCounterDaoServiceTest; +import org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicyTest; + +/** + * Version test suite + * + * @author Roy Wetherall + */ +public class VersionTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(VersionImplTest.class); + suite.addTestSuite(VersionHistoryImplTest.class); + suite.addTestSuite(SerialVersionLabelPolicyTest.class); + suite.addTestSuite(VersionCounterDaoServiceTest.class); + suite.addTestSuite(VersionServiceImplTest.class); + suite.addTestSuite(NodeServiceImplTest.class); + suite.addTestSuite(ContentServiceImplTest.class); + return suite; + } +} diff --git a/source/java/org/alfresco/repo/version/VersionableAspect.java b/source/java/org/alfresco/repo/version/VersionableAspect.java new file mode 100644 index 0000000000..d64c169ff9 --- /dev/null +++ b/source/java/org/alfresco/repo/version/VersionableAspect.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.evaluator.HasVersionHistoryEvaluator; +import org.alfresco.repo.action.executer.CreateVersionActionExecuter; +import org.alfresco.repo.policy.Behaviour; +import org.alfresco.repo.policy.BehaviourDefinition; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.repo.rule.RuntimeRuleService; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Class containing behaviour for the versionable aspect + * + * @author Roy Wetherall + */ +public class VersionableAspect +{ + /** + * The policy component + */ + private PolicyComponent policyComponent; + + /** + * The node service + */ + private NodeService nodeService; + + /** + * The rule service + */ + private RuleService ruleService; + + /** + * The action service + */ + private ActionService actionService; + + /** + * The rule used to create versions + */ + private Rule rule; + + /** + * Auto version behaviour + */ + private Behaviour autoVersionBehaviour; + + /** + * Set the policy component + * + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the rule service + * + * @param ruleService the rule service + */ + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + /** + * Set the action service + * + * @param actionService the action service + */ + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set the node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Initialise the versionable aspect policies + */ + public void init() + { + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + ContentModel.ASPECT_VERSIONABLE, + new JavaBehaviour(this, "onAddAspect")); + autoVersionBehaviour = new JavaBehaviour(this, "onContentUpdate"); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onContentUpdate"), + ContentModel.ASPECT_VERSIONABLE, + autoVersionBehaviour); + + // Register the copy behaviour + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyNode"), + ContentModel.ASPECT_VERSIONABLE, + new JavaBehaviour(this, "onCopy")); + + // Register the onCreateVersion behavior for the version aspect + //this.policyComponent.bindClassBehaviour( + // QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateVersion"), + // ContentModel.ASPECT_VERSIONABLE, + // new JavaBehaviour(this, "onCreateVersion")); + } + + /** + * OnCopy behaviour implementation for the version aspect. + *

    + * Ensures that the propety values of the version aspect are not copied onto + * the destination node. + * + * @see org.alfresco.repo.copy.CopyServicePolicies.OnCopyNodePolicy#onCopyNode(QName, NodeRef, StoreRef, boolean, PolicyScope) + */ + public void onCopy( + QName sourceClassRef, + NodeRef sourceNodeRef, + StoreRef destinationStoreRef, + boolean copyToNewNode, + PolicyScope copyDetails) + { + // Add the version aspect, but do not copy the version label + copyDetails.addAspect(ContentModel.ASPECT_VERSIONABLE); + copyDetails.addProperty( + ContentModel.ASPECT_VERSIONABLE, + ContentModel.PROP_AUTO_VERSION, + this.nodeService.getProperty(sourceNodeRef, ContentModel.PROP_AUTO_VERSION)); + } + + /** + * OnCreateVersion behaviour for the version aspect + *

    + * Ensures that the version aspect and it proerties are 'frozen' as part of + * the versioned state. + * + * @param classRef the class reference + * @param versionableNode the versionable node reference + * @param versionProperties the version properties + * @param nodeDetails the details of the node to be versioned + */ + public void onCreateVersion( + QName classRef, + NodeRef versionableNode, + Map versionProperties, + PolicyScope nodeDetails) + { + // Do nothing since we do not what to freeze any of the version + // properties + } + + + /** + * On add aspect policy behaviour + * + * @param nodeRef + * @param aspectTypeQName + */ + @SuppressWarnings("unchecked") + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + if (aspectTypeQName.equals(ContentModel.ASPECT_VERSIONABLE) == true) + { + // Queue create version action + queueCreateVersionAction(nodeRef); + } + } + + /** + * On content update policy bahaviour + * + * @param nodeRef the node reference + */ + public void onContentUpdate(NodeRef nodeRef) + { + if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == true) + { + // Determine whether the node is auto versionable or not + boolean autoVersion = false; + Boolean value = (Boolean)this.nodeService.getProperty(nodeRef, ContentModel.PROP_AUTO_VERSION); + if (value != null) + { + // If the value is not null then + autoVersion = value.booleanValue(); + } + // else this means that the default value has not been set and the versionable aspect was applied pre-1.1 + + if (autoVersion == true) + { + // Queue create version action + queueCreateVersionAction(nodeRef); + } + } + } + + /** + * Enable the auto version behaviour + * + */ + public void enableAutoVersion() + { + this.autoVersionBehaviour.enable(); + } + + /** + * Disable the auto version behaviour + * + */ + public void disableAutoVersion() + { + this.autoVersionBehaviour.disable(); + } + + /** + * Queue create version action + * + * @param nodeRef the node reference + */ + private void queueCreateVersionAction(NodeRef nodeRef) + { + if (this.rule == null) + { + this.rule = this.ruleService.createRule("inbound"); + Action action = this.actionService.createAction(CreateVersionActionExecuter.NAME); + ActionCondition condition = this.actionService.createActionCondition(HasVersionHistoryEvaluator.NAME); + condition.setInvertCondition(true); + action.addActionCondition(condition); + this.rule.addAction(action); + } + + ((RuntimeRuleService)this.ruleService).addRulePendingExecution(nodeRef, nodeRef, this.rule, true); + } +} diff --git a/source/java/org/alfresco/repo/version/common/AbstractVersionServiceImpl.java b/source/java/org/alfresco/repo/version/common/AbstractVersionServiceImpl.java new file mode 100644 index 0000000000..450891216b --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/AbstractVersionServiceImpl.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.repo.version.VersionServicePolicies; +import org.alfresco.repo.version.VersionServicePolicies.BeforeCreateVersionPolicy; +import org.alfresco.repo.version.VersionServicePolicies.CalculateVersionLabelPolicy; +import org.alfresco.repo.version.VersionServicePolicies.OnCreateVersionPolicy; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionServiceException; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * Abstract version service implementation. + * + * @author Roy Wetherall + */ +public abstract class AbstractVersionServiceImpl +{ + /** + * The common node service + */ + protected NodeService nodeService ; + + /** + * Policy component + */ + protected PolicyComponent policyComponent; + + /** + * The dictionary service + */ + protected DictionaryService dictionaryService; + + /** + * Policy delegates + */ + private ClassPolicyDelegate beforeCreateVersionDelegate; + private ClassPolicyDelegate onCreateVersionDelegate; + private ClassPolicyDelegate calculateVersionLabelDelegate; + + /** + * Sets the general node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the policy component + * + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Sets the dictionary service + * + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Initialise method + */ + public void initialise() + { + // Register the policies + this.beforeCreateVersionDelegate = this.policyComponent.registerClassPolicy(VersionServicePolicies.BeforeCreateVersionPolicy.class); + this.onCreateVersionDelegate = this.policyComponent.registerClassPolicy(VersionServicePolicies.OnCreateVersionPolicy.class); + this.calculateVersionLabelDelegate = this.policyComponent.registerClassPolicy(VersionServicePolicies.CalculateVersionLabelPolicy.class); + } + + /** + * Invokes the before create version policy behaviour + * + * @param nodeRef the node being versioned + */ + protected void invokeBeforeCreateVersion(NodeRef nodeRef) + { + // invoke for node type + QName nodeTypeQName = nodeService.getType(nodeRef); + this.beforeCreateVersionDelegate.get(nodeTypeQName).beforeCreateVersion(nodeRef); + // invoke for node aspects + Set nodeAspectQNames = nodeService.getAspects(nodeRef); + this.beforeCreateVersionDelegate.get(nodeAspectQNames).beforeCreateVersion(nodeRef); + } + + /** + * Invoke the on create version policy behaviour + * + */ + protected void invokeOnCreateVersion( + NodeRef nodeRef, + Map versionProperties, + PolicyScope nodeDetails) + { + // Sort out the policies for the node type + QName classRef = this.nodeService.getType(nodeRef); + invokeOnCreateVersion(classRef, nodeRef, versionProperties, nodeDetails); + + // Sort out the policies for the aspects + Collection aspects = this.nodeService.getAspects(nodeRef); + for (QName aspect : aspects) + { + invokeOnCreateVersion(aspect, nodeRef, versionProperties, nodeDetails); + } + + } + + /** + * Invokes the on create version policy behaviour for a given type + * + * @param classRef + * @param nodeDetails + * @param nodeRef + * @param versionProperties + */ + private void invokeOnCreateVersion( + QName classRef, + NodeRef nodeRef, + Map versionProperties, + PolicyScope nodeDetails) + { + Collection policies = this.onCreateVersionDelegate.getList(classRef); + if (policies.size() == 0) + { + // Call the default implementation + defaultOnCreateVersion( + classRef, + nodeRef, + versionProperties, + nodeDetails); + } + else + { + // Call the policy definitions + for (VersionServicePolicies.OnCreateVersionPolicy policy : policies) + { + policy.onCreateVersion( + classRef, + nodeRef, + versionProperties, + nodeDetails); + } + } + } + + /** + * Default implementation of the on create version policy. Called if no behaviour is registered for the + * policy for the specified type. + * + * @param nodeRef + * @param versionProperties + * @param nodeDetails + */ + protected void defaultOnCreateVersion( + QName classRef, + NodeRef nodeRef, + Map versionProperties, + PolicyScope nodeDetails) + { + ClassDefinition classDefinition = this.dictionaryService.getClass(classRef); + if (classDefinition != null) + { + // Copy the properties + Map propertyDefinitions = classDefinition.getProperties(); + for (QName propertyName : propertyDefinitions.keySet()) + { + Serializable propValue = this.nodeService.getProperty(nodeRef, propertyName); + nodeDetails.addProperty(classRef, propertyName, propValue); + } + + // Version the associations (child and target) + Map assocDefs = classDefinition.getAssociations(); + + // TODO: Need way of getting child assocs of a given type + if (classDefinition.isContainer()) + { + List childAssocRefs = this.nodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + if (assocDefs.containsKey(childAssocRef.getTypeQName())) + { + nodeDetails.addChildAssociation(classDefinition.getName(), childAssocRef); + } + } + } + + // TODO: Need way of getting assocs of a given type + List nodeAssocRefs = this.nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL); + for (AssociationRef nodeAssocRef : nodeAssocRefs) + { + if (assocDefs.containsKey(nodeAssocRef.getTypeQName())) + { + nodeDetails.addAssociation(classDefinition.getName(), nodeAssocRef); + } + } + } + } + + /** + * Invoke the calculate version label policy behaviour + * + * @param classRef + * @param preceedingVersion + * @param versionNumber + * @param versionProperties + * @return + */ + protected String invokeCalculateVersionLabel( + QName classRef, + Version preceedingVersion, + int versionNumber, + MapversionProperties) + { + String versionLabel = null; + + Collection behaviours = this.calculateVersionLabelDelegate.getList(classRef); + if (behaviours.size() == 0) + { + // Default the version label to the version numbder + versionLabel = Integer.toString(versionNumber); + } + else if (behaviours.size() == 1) + { + // Call the policy behaviour + CalculateVersionLabelPolicy[] arr = behaviours.toArray(new CalculateVersionLabelPolicy[]{}); + versionLabel = arr[0].calculateVersionLabel(classRef, preceedingVersion, versionNumber, versionProperties); + } + else + { + // Error since we can only deal with a single caculate version label policy + throw new VersionServiceException("More than one CalculateVersionLabelPolicy behaviour has been registered for the type " + classRef.toString()); + } + + return versionLabel; + } + +} diff --git a/source/java/org/alfresco/repo/version/common/VersionHistoryImpl.java b/source/java/org/alfresco/repo/version/common/VersionHistoryImpl.java new file mode 100644 index 0000000000..713c4bfa66 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/VersionHistoryImpl.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; + +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionDoesNotExistException; +import org.alfresco.service.cmr.version.VersionHistory; +import org.alfresco.service.cmr.version.VersionServiceException; + +/** + * Version History implementation. + * + * @author Roy Wetherall + */ +public class VersionHistoryImpl implements VersionHistory +{ + /* + * Serial version UID + */ + private static final long serialVersionUID = 3257001051558326840L; + + /* + * Error message(s) + */ + private static final String ERR_MSG = "The root version must be specified when creating a version history object."; + + /* + * The root version label + */ + private String rootVersionLabel = null; + + /* + * Version history tree structure map + */ + private HashMap versionHistory = null; + + /* + * Label to version object map + */ + private HashMap versions = null; + + private Version rootVersion; + + /** + * Constructor, ensures the root version is set. + * + * @param rootVersion the root version, can not be null. + */ + public VersionHistoryImpl(Version rootVersion) + { + if (rootVersion == null) + { + // Exception - a version history can not be created unless + // a root version is specified + throw new VersionServiceException(VersionHistoryImpl.ERR_MSG); + } + + this.versionHistory = new HashMap(); + this.versions = new HashMap(); + + this.rootVersion = rootVersion; + this.rootVersionLabel = rootVersion.getVersionLabel(); + addVersion(rootVersion, null); + } + + /** + * Gets the root (or initial) version of the version history. + * + * @return the root version + */ + public Version getRootVersion() + { + return this.rootVersion; + } + + /** + * Gets a collection containing all the versions within the + * version history. + *

    + * The order of the versions is not guarenteed. + * + * @return collection containing all the versions + */ + public Collection getAllVersions() + { + return this.versions.values(); + } + + /** + * Gets the predecessor of a specified version + * + * @param version the version object + * @return the predeceeding version, null if root version + */ + public Version getPredecessor(Version version) + { + Version result = null; + if (version != null) + { + result = getVersion(this.versionHistory.get(version.getVersionLabel())); + } + return result; + } + + /** + * Gets the succeeding versions of a specified version. + * + * @param version the version object + * @return a collection containing the succeeding version, empty is none + */ + public Collection getSuccessors(Version version) + { + ArrayList result = new ArrayList(); + + if (version != null) + { + String versionLabel = version.getVersionLabel(); + + if (this.versionHistory.containsValue(versionLabel) == true) + { + for (String key : this.versionHistory.keySet()) + { + if (this.versionHistory.get(key) == versionLabel) + { + result.add(getVersion(key)); + } + } + } + } + + return result; + } + + /** + * Gets a version with a specified version label. The version label is guarenteed + * unique within the version history. + * + * @param versionLabel the version label + * @return the version object + * @throws VersionDoesNotExistException indicates requested version does not exisit + */ + public Version getVersion(String versionLabel) + { + Version result = null; + if (versionLabel != null) + { + result = this.versions.get(versionLabel); + + if (result == null) + { + // Throw exception indicating that the version does not exit + throw new VersionDoesNotExistException(versionLabel); + } + } + return result; + } + + /** + * Add a version to the version history. + *

    + * Used internally to build the version history tree. + * + * @param version the version object + * @param predecessor the preceeding version + */ + public void addVersion(Version version, Version predecessor) + { + // TODO cope with exception case where duplicate version labels have been specified + + this.versions.put(version.getVersionLabel(), version); + + if (predecessor != null) + { + this.versionHistory.put(version.getVersionLabel(), predecessor.getVersionLabel()); + } + } +} diff --git a/source/java/org/alfresco/repo/version/common/VersionHistoryImplTest.java b/source/java/org/alfresco/repo/version/common/VersionHistoryImplTest.java new file mode 100644 index 0000000000..f77db33711 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/VersionHistoryImplTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionDoesNotExistException; +import org.alfresco.service.cmr.version.VersionServiceException; + +/** + * VersionHistoryImpl Unit Test Class + * + * @author Roy Wetherall + */ +public class VersionHistoryImplTest extends TestCase +{ + /** + * Data used in the tests + */ + private Version rootVersion = null; + private Version childVersion1 = null; + private Version childVersion2 = null; + + /** + * Set up + */ + protected void setUp() throws Exception + { + super.setUp(); + + // Create dummy node ref + NodeRef nodeRef = new NodeRef(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "test"), "test"); + + HashMap versionProperties1 = new HashMap(); + versionProperties1.put(VersionModel.PROP_VERSION_LABEL, "1"); + versionProperties1.put(VersionModel.PROP_CREATED_DATE, new Date()); + versionProperties1.put("testProperty", "testValue"); + this.rootVersion = new VersionImpl(versionProperties1, nodeRef); + + HashMap versionProperties2 = new HashMap(); + versionProperties2.put(VersionModel.PROP_VERSION_LABEL, "2"); + versionProperties2.put(VersionModel.PROP_CREATED_DATE, new Date()); + versionProperties2.put("testProperty", "testValue"); + this.childVersion1 = new VersionImpl(versionProperties2, nodeRef); + + HashMap versionProperties3 = new HashMap(); + versionProperties3.put(VersionModel.PROP_VERSION_LABEL, "3"); + versionProperties3.put(VersionModel.PROP_CREATED_DATE, new Date()); + versionProperties3.put("testProperty", "testValue"); + this.childVersion2 = new VersionImpl(versionProperties3, nodeRef); + } + + /** + * Test constructor + */ + public void testConstructor() + { + testContructorImpl(); + } + + /** + * Test construtor helper + * + * @return new version history + */ + private VersionHistoryImpl testContructorImpl() + { + VersionHistoryImpl vh = new VersionHistoryImpl(this.rootVersion); + assertNotNull(vh); + + return vh; + } + + /** + * Exception case - a root version must be specified when creating a + * version history object + */ + public void testRootVersionSpecified() + { + try + { + new VersionHistoryImpl(null); + fail(); + } + catch(VersionServiceException exception) + { + } + } + + /** + * Test getRootVersion + * + *@return root version + */ + public void testGetRootVersion() + { + VersionHistoryImpl vh = testContructorImpl(); + + Version rootVersion = vh.getRootVersion(); + assertNotNull(rootVersion); + assertEquals(rootVersion, this.rootVersion); + } + + /** + * Test getAllVersions + */ + public void testGetAllVersions() + { + VersionHistoryImpl vh = testAddVersionImpl(); + + Collection allVersions = vh.getAllVersions(); + assertNotNull(allVersions); + assertEquals(3, allVersions.size()); + } + + /** + * Test addVersion + * + * @return version history + */ + public void testAddVersion() + { + testAddVersionImpl(); + } + + /** + * Test addVersion helper + * + * @return version history with version tree built + */ + private VersionHistoryImpl testAddVersionImpl() + { + VersionHistoryImpl vh = testContructorImpl(); + Version rootVersion = vh.getRootVersion(); + + vh.addVersion(this.childVersion1, rootVersion); + vh.addVersion(this.childVersion2, rootVersion); + + return vh; + } + + /** + * TODO Exception case - add version that has already been added + */ + + /** + * TODO Exception case - add a version with a duplicate version label + */ + + /** + * Test getPredecessor + */ + public void testGetPredecessor() + { + VersionHistoryImpl vh = testAddVersionImpl(); + + Version version1 = vh.getPredecessor(this.childVersion1); + assertEquals(version1.getVersionLabel(), this.rootVersion.getVersionLabel()); + + Version version2 = vh.getPredecessor(this.childVersion2); + assertEquals(version2.getVersionLabel(), this.rootVersion.getVersionLabel()); + + Version version3 = vh.getPredecessor(this.rootVersion); + assertNull(version3); + + try + { + Version version4 = vh.getPredecessor(null); + assertNull(version4); + } + catch (Exception exception) + { + fail("Should continue by returning null."); + } + } + + /** + * Test getSuccessors + */ + public void testGetSuccessors() + { + VersionHistoryImpl vh = testAddVersionImpl(); + + Collection versions1 = vh.getSuccessors(this.rootVersion); + assertNotNull(versions1); + assertEquals(versions1.size(), 2); + + for (Version version : versions1) + { + String versionLabel = version.getVersionLabel(); + if (!(versionLabel == "2" || versionLabel == "3")) + { + fail("There is a version in this collection that should not be here."); + } + } + + Collection versions2 = vh.getSuccessors(this.childVersion1); + assertNotNull(versions2); + assertTrue(versions2.isEmpty()); + + Collection versions3 = vh.getSuccessors(this.childVersion2); + assertNotNull(versions3); + assertTrue(versions3.isEmpty()); + } + + /** + * Test getVersion + */ + public void testGetVersion() + { + VersionHistoryImpl vh = testAddVersionImpl(); + + Version version1 = vh.getVersion("1"); + assertEquals(version1.getVersionLabel(), this.rootVersion.getVersionLabel()); + + Version version2 = vh.getVersion("2"); + assertEquals(version2.getVersionLabel(), this.childVersion1.getVersionLabel()); + + Version version3 = vh.getVersion("3"); + assertEquals(version3.getVersionLabel(), this.childVersion2.getVersionLabel()); + + try + { + vh.getVersion("invalidLabel"); + fail("An exception should have been thrown if the version can not be retrieved."); + } + catch (VersionDoesNotExistException exception) + { + System.out.println("Error message: " + exception.getMessage()); + } + } +} diff --git a/source/java/org/alfresco/repo/version/common/VersionImpl.java b/source/java/org/alfresco/repo/version/common/VersionImpl.java new file mode 100644 index 0000000000..43a6551bf7 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/VersionImpl.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConverter; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionServiceException; +import org.alfresco.service.cmr.version.VersionType; + + +/** + * Version class implementation. + * + * Used to represent the data about a version stored in a version store. + * + * @author Roy Wetherall + */ +public class VersionImpl implements Version +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3257567304324888881L; + + /** + * Error message(s) + */ + private static final String ERR_NO_NODE_REF = "A valid node reference must be supplied when creating a verison."; + + /** + * The properties of the version + */ + private Map versionProperties = null; + + /** + * The node reference that represents the frozen state of the versioned object + */ + private NodeRef nodeRef = null; + + /** + * Constructor that initialises the state of the version object. + * + * @param versionProperties the version properties + * @param nodeRef the forzen state node reference + */ + public VersionImpl( + Map versionProperties, + NodeRef nodeRef) + { + if (nodeRef == null) + { + // Exception - a node ref must be specified + throw new VersionServiceException(VersionImpl.ERR_NO_NODE_REF); + } + + this.versionProperties = versionProperties; + this.nodeRef = nodeRef; + } + + + /** + * Helper method to get the created date from the version property data. + * + * @return the date the version was created + */ + public Date getCreatedDate() + { + return (Date)this.versionProperties.get(VersionModel.PROP_CREATED_DATE); + } + + public String getCreator() + { + return (String)this.versionProperties.get(VersionModel.PROP_CREATOR); + } + + /** + * Helper method to get the version label from the version property data. + * + * @return the version label + */ + public String getVersionLabel() + { + return (String)this.versionProperties.get(VersionModel.PROP_VERSION_LABEL); + } + + /** + * Helper method to get the version type. + * + * @return the value of the version type as an enum value + */ + public VersionType getVersionType() + { + return (VersionType)this.versionProperties.get(VersionModel.PROP_VERSION_TYPE); + } + + /** + * Helper method to get the version description. + * + * @return the version description + */ + public String getDescription() + { + return (String)this.versionProperties.get(PROP_DESCRIPTION); + } + + /** + * @see org.alfresco.service.cmr.version.Version#getVersionProperties() + */ + public Map getVersionProperties() + { + return this.versionProperties; + } + + /** + * @see org.alfresco.service.cmr.version.Version#getVersionProperty(java.lang.String) + */ + public Serializable getVersionProperty(String name) + { + Serializable result = null; + if (this.versionProperties != null) + { + result = this.versionProperties.get(name); + } + return result; + } + + /** + * @see org.alfresco.service.cmr.version.Version#getVersionedNodeRef() + */ + public NodeRef getVersionedNodeRef() + { + String storeProtocol = (String)this.versionProperties.get(VersionModel.PROP_FROZEN_NODE_STORE_PROTOCOL); + String storeId = (String)this.versionProperties.get(VersionModel.PROP_FROZEN_NODE_STORE_ID); + String nodeId = (String)this.versionProperties.get(VersionModel.PROP_FROZEN_NODE_ID); + return new NodeRef(new StoreRef(storeProtocol, storeId), nodeId); + } + + /** + * @see org.alfresco.service.cmr.version.Version#getFrozenStateNodeRef() + */ + public NodeRef getFrozenStateNodeRef() + { + return this.nodeRef; + } + + /** + * Static block to register the version type converters + */ + static + { + DefaultTypeConverter.INSTANCE.addConverter( + String.class, + VersionType.class, + new TypeConverter.Converter() + { + public VersionType convert(String source) + { + return VersionType.valueOf(source); + } + + }); + + DefaultTypeConverter.INSTANCE.addConverter( + VersionType.class, + String.class, + new TypeConverter.Converter() + { + public String convert(VersionType source) + { + return source.toString(); + } + + }); + } + } diff --git a/source/java/org/alfresco/repo/version/common/VersionImplTest.java b/source/java/org/alfresco/repo/version/common/VersionImplTest.java new file mode 100644 index 0000000000..deeca0260c --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/VersionImplTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionServiceException; +import org.alfresco.service.cmr.version.VersionType; + +import junit.framework.TestCase; + +/** + * VersionImpl Unit Test + * + * @author Roy Wetherall + */ +public class VersionImplTest extends TestCase +{ + /** + * Property names and values + */ + private final static String PROP_1 = "prop1"; + private final static String PROP_2 = "prop2"; + private final static String PROP_3 = "prop3"; + private final static String VALUE_1 = "value1"; + private final static String VALUE_2 = "value2"; + private final static String VALUE_3 = "value3"; + private final static String VALUE_DESCRIPTION = "This string describes the version details."; + private final static VersionType VERSION_TYPE = VersionType.MINOR; + private final static String USER_NAME = "userName"; + + /** + * Version labels + */ + private final static String VERSION_1 = "1"; + + /** + * Data used during tests + */ + private VersionImpl version = null; + private NodeRef nodeRef = null; + private Map versionProperties = null; + private Date createdDate = new Date(); + + /** + * Test case set up + */ + protected void setUp() throws Exception + { + super.setUp(); + + // Create the node reference + this.nodeRef = new NodeRef(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "testWS"), "testID"); + assertNotNull(this.nodeRef); + + // Create the version property map + this.versionProperties = new HashMap(); + this.versionProperties.put(VersionModel.PROP_VERSION_LABEL, VERSION_1); + this.versionProperties.put(VersionModel.PROP_CREATED_DATE, this.createdDate); + this.versionProperties.put(VersionModel.PROP_CREATOR, USER_NAME); + this.versionProperties.put(Version.PROP_DESCRIPTION, VALUE_DESCRIPTION); + this.versionProperties.put(VersionModel.PROP_VERSION_TYPE, VERSION_TYPE); + this.versionProperties.put(PROP_1, VALUE_1); + this.versionProperties.put(PROP_2, VALUE_2); + this.versionProperties.put(PROP_3, VALUE_3); + + // Create the root version + this.version = new VersionImpl(this.versionProperties, this.nodeRef); + assertNotNull(this.version); + } + + + /** + * Test getCreatedDate() + */ + public void testGetCreatedDate() + { + Date createdDate1 = this.version.getCreatedDate(); + assertEquals(this.createdDate, createdDate1); + } + + /** + * Test getCreator + */ + public void testGetCreator() + { + assertEquals(USER_NAME, this.version.getCreator()); + } + + /** + * Test getVersionLabel() + */ + public void testGetVersionLabel() + { + String versionLabel1 = this.version.getVersionLabel(); + assertEquals(VersionImplTest.VERSION_1, versionLabel1); + } + + /** + * Test getDescription + */ + public void testGetDescription() + { + String description = this.version.getDescription(); + assertEquals(VALUE_DESCRIPTION, description); + } + + /** + * Test getVersionType + */ + public void testGetVersionType() + { + VersionType versionType = this.version.getVersionType(); + assertEquals(VERSION_TYPE, versionType); + } + + /** + * Test getVersionProperties + * + */ + public void testGetVersionProperties() + { + Map versionProperties = version.getVersionProperties(); + assertNotNull(versionProperties); + assertEquals(this.versionProperties.size(), versionProperties.size()); + } + + /** + * Test getVersionProperty + */ + public void testGetVersionProperty() + { + String value1 = (String)version.getVersionProperty(VersionImplTest.PROP_1); + assertEquals(value1, VersionImplTest.VALUE_1); + + String value2 = (String)version.getVersionProperty(VersionImplTest.PROP_2); + assertEquals(value2, VersionImplTest.VALUE_2); + + String value3 = (String)version.getVersionProperty(VersionImplTest.PROP_3); + assertEquals(value3, VersionImplTest.VALUE_3); + } + + /** + * Test getNodeRef() + */ + public void testGetNodeRef() + { + NodeRef nodeRef = this.version.getFrozenStateNodeRef(); + assertNotNull(nodeRef); + assertEquals(nodeRef.toString(), this.nodeRef.toString()); + } + + /** + * Exception case - no node ref supplied when creating a verison + */ + public void testNoNodeRefOnVersionCreate() + { + try + { + new VersionImpl(this.versionProperties, null); + fail("It is invalid to create a version object without a node ref specified."); + } + catch (VersionServiceException exception) + { + } + } +} diff --git a/source/java/org/alfresco/repo/version/common/VersionUtil.java b/source/java/org/alfresco/repo/version/common/VersionUtil.java new file mode 100644 index 0000000000..5bd6d5a6f9 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/VersionUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common; + +import java.util.Collection; + +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.version.ReservedVersionNameException; + +/** + * Helper class containing helper methods for the versioning services. + * + * @author Roy Wetherall + */ +public class VersionUtil +{ + /** + * Reserved property names + */ + public static final String[] RESERVED_PROPERTY_NAMES = new String[]{ + VersionModel.PROP_CREATED_DATE, + VersionModel.PROP_FROZEN_NODE_ID, + VersionModel.PROP_FROZEN_NODE_STORE_ID, + VersionModel.PROP_FROZEN_NODE_STORE_PROTOCOL, + VersionModel.PROP_FROZEN_NODE_TYPE, + VersionModel.PROP_FROZEN_ASPECTS, + VersionModel.PROP_VERSION_LABEL, + VersionModel.PROP_VERSION_NUMBER}; + + /** + * Checks that the names of the additional version properties are valid and that they do not clash + * with the reserved properties. + * + * @param versionProperties the property names + * @return true is the names are considered valid, false otherwise + * @throws ReservedVersionNameException + */ + public static void checkVersionPropertyNames(Collection names) + throws ReservedVersionNameException + { + for (String name : RESERVED_PROPERTY_NAMES) + { + if (names.contains(name) == true) + { + throw new ReservedVersionNameException(name); + } + } + } +} diff --git a/source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoService.java b/source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoService.java new file mode 100644 index 0000000000..8346d65803 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoService.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common.counter; + +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * Version counter DAO service interface. + * + * @author Roy Wetherall + */ +public interface VersionCounterDaoService +{ + /** + * Get the next available version number for the specified store. + * + * @param storeRef the store reference + * @return the next version number + */ + public int nextVersionNumber(StoreRef storeRef); + + /** + * Gets the current version number for the specified store. + * + * @param storeRef the store reference + * @return the current versio number + */ + public int currentVersionNumber(StoreRef storeRef); + + /** + * Resets the version number for a the specified store. + * + * WARNING: calling this method will completely reset the current + * version count for the specified store and cannot be undone. + * + * @param storeRef the store reference + */ + public void resetVersionNumber(StoreRef storeRef); +} diff --git a/source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoServiceTest.java b/source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoServiceTest.java new file mode 100644 index 0000000000..3990dcd2e0 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/counter/VersionCounterDaoServiceTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common.counter; + +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.BaseSpringTest; + +/** + * @author Roy Wetherall + */ +public class VersionCounterDaoServiceTest extends BaseSpringTest +{ + /* + * Test store id's + */ + private final static String STORE_ID_1 = "test1_" + System.currentTimeMillis(); + private final static String STORE_ID_2 = "test2_" + System.currentTimeMillis(); + private static final String STORE_NONE = "test3_" + System.currentTimeMillis();; + + private NodeService nodeService; + private VersionCounterDaoService counter; + + @Override + public void onSetUpInTransaction() + { + nodeService = (NodeService) applicationContext.getBean("dbNodeService"); + counter = (VersionCounterDaoService) applicationContext.getBean("versionCounterDaoService"); + } + + public void testSetUp() throws Exception + { + assertNotNull(nodeService); + assertNotNull(counter); + } + + /** + * Test nextVersionNumber + */ + public void testNextVersionNumber() + { + // Create the store references + StoreRef store1 = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, VersionCounterDaoServiceTest.STORE_ID_1); + StoreRef store2 = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, VersionCounterDaoServiceTest.STORE_ID_2); + StoreRef storeNone = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, VersionCounterDaoServiceTest.STORE_NONE); + + int store1Version0 = this.counter.nextVersionNumber(store1); + assertEquals(store1Version0, 1); + + int store1Version1 = this.counter.nextVersionNumber(store1); + assertEquals(store1Version1, 2); + + int store2Version0 = this.counter.nextVersionNumber(store2); + assertEquals(store2Version0, 1); + + int store1Version2 = this.counter.nextVersionNumber(store1); + assertEquals(store1Version2, 3); + + int store2Version1 = this.counter.nextVersionNumber(store2); + assertEquals(store2Version1, 2); + + int store1Current = this.counter.currentVersionNumber(store1); + assertEquals(store1Current, 3); + + int store2Current = this.counter.currentVersionNumber(store2); + assertEquals(store2Current, 2); + + int storeNoneCurrent = this.counter.currentVersionNumber(storeNone); + assertEquals(storeNoneCurrent, 0); + + // Need to clean-up since the version counter works in its own transaction + this.counter.resetVersionNumber(store1); + this.counter.resetVersionNumber(store2); + } + +} diff --git a/source/java/org/alfresco/repo/version/common/counter/hibernate/HibernateVersionCounterDaoServiceImpl.java b/source/java/org/alfresco/repo/version/common/counter/hibernate/HibernateVersionCounterDaoServiceImpl.java new file mode 100644 index 0000000000..027219f08b --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/counter/hibernate/HibernateVersionCounterDaoServiceImpl.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common.counter.hibernate; + +import java.util.concurrent.locks.Lock; + +import org.alfresco.repo.domain.StoreKey; +import org.alfresco.repo.domain.VersionCount; +import org.alfresco.repo.domain.hibernate.VersionCountImpl; +import org.alfresco.repo.version.common.counter.VersionCounterDaoService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * Version counter DAO service implemtation using Hibernate. + *

    + * The object should execute within its own transaction, and is limited to single-thread + * entry. If it becomes a bottleneck, the transaction synchronization should be moved + * over to reentrant locks and/or the hibernate mappings should be optimized for better + * read-write access. + * + * @author Derek Hulley + */ +public class HibernateVersionCounterDaoServiceImpl extends HibernateDaoSupport implements VersionCounterDaoService +{ + private Lock countReadLock; + private Lock countWriteLock; + + /** + * Retrieves or creates a version counter + * + * @param storeKey + * @return Returns a current or new version counter + */ + private VersionCount getVersionCounter(StoreRef storeRef) + { + StoreKey storeKey = new StoreKey(storeRef.getProtocol(), storeRef.getIdentifier()); + // get the version counter + VersionCount versionCounter = (VersionCount) getHibernateTemplate().get(VersionCountImpl.class, storeKey); + // check if it exists + if (versionCounter == null) + { + // create a new one + versionCounter = new VersionCountImpl(); + getHibernateTemplate().save(versionCounter, storeKey); + } + return versionCounter; + } + + /** + * Get the next available version number for the specified store. + * + * @param storeRef the version store id + * @return the next version number + */ + public synchronized int nextVersionNumber(StoreRef storeRef) + { + // get the version counter + VersionCount versionCounter = getVersionCounter(storeRef); + // get an incremented count + return versionCounter.incrementVersionCount(); + } + + /** + * Gets the current version number for the specified store. + * + * @param storeRef the store reference + * @return the current version number, zero if no version yet allocated. + */ + public synchronized int currentVersionNumber(StoreRef storeRef) + { + // get the version counter + VersionCount versionCounter = getVersionCounter(storeRef); + // get an incremented count + return versionCounter.getVersionCount(); + } + + /** + * Resets the version number for a the specified store. + * + * WARNING: calling this method will completely reset the current + * version count for the specified store and cannot be undone. + * + * @param storeRef the store reference + */ + public synchronized void resetVersionNumber(StoreRef storeRef) + { + // get the version counter + VersionCount versionCounter = getVersionCounter(storeRef); + // get an incremented count + versionCounter.resetVersionCount(); + } +} diff --git a/source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicy.java b/source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicy.java new file mode 100644 index 0000000000..cfaefe1f17 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicy.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common.versionlabel; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.QName; + +/** + * The serial version label policy. + * + * @author Roy Wetherall + */ +public class SerialVersionLabelPolicy +{ + // TODO need to add support for branches into this labeling policy + + /** + * Get the version label value base on the data provided. + * + * @param preceedingVersion the preceeding version, null if none + * @param versionNumber the new version number + * @param versionProperties the version property values + * @return the version label + */ + public String calculateVersionLabel( + QName classRef, + Version preceedingVersion, + int versionNumber, + Map versionProperties) + { + SerialVersionLabel serialVersionNumber = null; + + if (preceedingVersion != null) + { + serialVersionNumber = new SerialVersionLabel(preceedingVersion.getVersionLabel()); + + VersionType versionType = (VersionType)versionProperties.get(VersionModel.PROP_VERSION_TYPE); + if (VersionType.MAJOR.equals(versionType) == true) + { + serialVersionNumber.majorIncrement(); + } + else + { + serialVersionNumber.minorIncrement(); + } + } + else + { + serialVersionNumber = new SerialVersionLabel(null); + } + + return serialVersionNumber.toString(); + } + + /** + * Inner class encapsulating the notion of the serial version number. + * + * @author Roy Wetherall + */ + private class SerialVersionLabel + { + /** + * The version number delimiter + */ + private static final String DELIMITER = "."; + + /** + * The major revision number + */ + private int majorRevisionNumber = 1; + + /** + * The minor revision number + */ + private int minorRevisionNumber = 0; + + /** + * Constructor + * + * @param version the vesion to take the version from + */ + public SerialVersionLabel(String versionLabel) + { + if (versionLabel != null && versionLabel.length() != 0) + { + int iIndex = versionLabel.indexOf(DELIMITER); + String majorString = versionLabel.substring(0, iIndex); + String minorString = versionLabel.substring(iIndex+1); + + this.majorRevisionNumber = Integer.parseInt(majorString); + this.minorRevisionNumber = Integer.parseInt(minorString); + } + } + + /** + * Increments the major revision numebr and sets the minor to + * zero. + */ + public void majorIncrement() + { + this.majorRevisionNumber += 1; + this.minorRevisionNumber = 0; + } + + /** + * Increments only the minor revision number + */ + public void minorIncrement() + { + this.minorRevisionNumber += 1; + } + + /** + * Converts the serial version number into a string + */ + public String toString() + { + return this.majorRevisionNumber + DELIMITER + this.minorRevisionNumber; + } + } +} diff --git a/source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicyTest.java b/source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicyTest.java new file mode 100644 index 0000000000..5233de1956 --- /dev/null +++ b/source/java/org/alfresco/repo/version/common/versionlabel/SerialVersionLabelPolicyTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.version.common.versionlabel; + +import java.io.Serializable; +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.repo.version.common.VersionImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionType; + +/** + * Unit test class for SerialVersionLabelPolicy class + * + * @author Roy Wetherall + */ +public class SerialVersionLabelPolicyTest extends TestCase +{ + /** + * Test getVersionLabelValue + */ + public void testGetVersionLabelValue() + { + SerialVersionLabelPolicy policy = new SerialVersionLabelPolicy(); + + NodeRef dummyNodeRef = new NodeRef(new StoreRef("", ""), ""); + + HashMap versionProp1 = new HashMap(); + versionProp1.put(VersionModel.PROP_VERSION_TYPE, VersionType.MINOR); + + String initialVersion = policy.calculateVersionLabel( + ContentModel.TYPE_CMOBJECT, + null, + 0, + versionProp1); + assertEquals("1.0", initialVersion); + + HashMap versionProp2 = new HashMap(); + versionProp2.put(VersionModel.PROP_VERSION_LABEL, "1.0"); + Version version1 = new VersionImpl(versionProp2, dummyNodeRef); + + String verisonLabel1 = policy.calculateVersionLabel( + ContentModel.TYPE_CMOBJECT, + version1, + 1, + versionProp1); + assertEquals("1.1", verisonLabel1); + + HashMap versionProp3 = new HashMap(); + versionProp3.put(VersionModel.PROP_VERSION_LABEL, "1.1"); + Version version2 = new VersionImpl(versionProp3, dummyNodeRef); + + HashMap versionProp4 = new HashMap(); + versionProp4.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR); + + String verisonLabel2 = policy.calculateVersionLabel( + ContentModel.TYPE_CMOBJECT, + version2, + 1, + versionProp4); + assertEquals("2.0", verisonLabel2); + } + +} diff --git a/source/java/org/alfresco/repo/version/version_model.xml b/source/java/org/alfresco/repo/version/version_model.xml new file mode 100644 index 0000000000..68d903fb33 --- /dev/null +++ b/source/java/org/alfresco/repo/version/version_model.xml @@ -0,0 +1,156 @@ + + + Alfresco Version Store Model + Alfresco + 2005-05-30 + 0.1 + + + + + + + + + + + + + + + sys:base + + + d:text + + + d:any + + + + + + sys:base + + + d:qname + + + d:any + + + d:any + true + + + d:boolean + + + + + + sys:reference + + + d:qname + + + + + + ver:versionedAssoc + + + d:boolean + + + d:int + + + + + + sys:container + + + d:int + + + d:text + + + d:text + + + d:text + + + d:text + + + d:qname + + + d:qname + true + + + + + + ver:versionMetaDataValue + + + + + ver:versionedProperty + + + + + ver:versionedChildAssoc + + + + + ver:versionedAssoc + + + + + ver:version + + + + + + cm:auditable + + + + + sys:base + + + + d:text + + + + + + + ver:version + + + + + ver:version + + + + + + + + diff --git a/source/java/org/alfresco/service/ServiceDescriptor.java b/source/java/org/alfresco/service/ServiceDescriptor.java new file mode 100644 index 0000000000..a10bb33309 --- /dev/null +++ b/source/java/org/alfresco/service/ServiceDescriptor.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service; + +import java.util.Collection; + +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + + +/** + * This interface represents service meta-data. + * + * @author David Caruana + */ +public interface ServiceDescriptor +{ + /** + * @return the qualified name of the service + */ + public QName getQualifiedName(); + + /** + * @return the service description + */ + public String getDescription(); + + /** + * @return the service interface class description + */ + public Class getInterface(); + + /** + * @return the names of the protocols supported + */ + public Collection getSupportedStoreProtocols(); + + /** + * @return the Store Refs of the stores supported + */ + public Collection getSupportedStores(); +} diff --git a/source/java/org/alfresco/service/ServiceException.java b/source/java/org/alfresco/service/ServiceException.java new file mode 100644 index 0000000000..55cb62c14e --- /dev/null +++ b/source/java/org/alfresco/service/ServiceException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service; + + +/** + * Base Exception of Service Exceptions. + * + * @author David Caruana + */ +public class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 3257008761007847733L; + + public ServiceException(String msg) + { + super(msg); + } + + public ServiceException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/service/ServiceRegistry.java b/source/java/org/alfresco/service/ServiceRegistry.java new file mode 100644 index 0000000000..5cd8917780 --- /dev/null +++ b/source/java/org/alfresco/service/ServiceRegistry.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service; + +import java.util.Collection; + +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; + + +/** + * This interface represents the registry of public Repository Services. + * The registry provides meta-data about each service and provides + * access to the service interface. + * + * @author David Caruana + */ +public interface ServiceRegistry +{ + // Service Bean Names + + static final String SERVICE_REGISTRY = "ServiceRegistry"; + + static final QName REGISTRY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "ServiceRegistry"); + static final QName DESCRIPTOR_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "DescriptorService"); + static final QName TRANSACTION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "TransactionService"); + static final QName AUTHENTICATION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "AuthenticationService"); + static final QName NAMESPACE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "NamespaceService"); + static final QName DICTIONARY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "DictionaryService"); + static final QName NODE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "NodeService"); + static final QName CONTENT_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "ContentService"); + static final QName MIMETYPE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "MimetypeService"); + static final QName SEARCH_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "SearchService"); + static final QName CATEGORY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "CategoryService"); + static final QName COPY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "CopyService"); + static final QName LOCK_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "LockService"); + static final QName VERSION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "VersionService"); + static final QName COCI_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "CheckoutCheckinService"); + static final QName RULE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RuleService"); + static final QName IMPORTER_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "ImporterService"); + static final QName EXPORTER_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "ExporterService"); + static final QName ACTION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "ActionService"); + static final QName PERMISSIONS_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "PermissionService"); + static final QName AUTHORITY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "AuthorityService"); + static final QName TEMPLATE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "TemplateService"); + static final QName FILE_FOLDER_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "FileFolderService"); + + /** + * Get the list of services provided by the Repository + * + * @return list of provided Services + */ + Collection getServices(); + + /** + * Is the specified service provided by the Repository? + * + * @param service name of service to test provision of + * @return true => provided, false => not provided + */ + boolean isServiceProvided(QName service); + + /** + * Get meta-data about the specified service + * + * @param service name of service to retrieve meta data for + * @return the service meta data + */ + ServiceDescriptor getServiceDescriptor(QName service); + + /** + * Get the specified service. + * + * @param service name of service to retrieve + * @return the service interface (must cast to interface as described in service meta-data) + */ + Object getService(QName service); + + /** + * @return the descriptor service + */ + DescriptorService getDescriptorService(); + + /** + * @return the transaction service + */ + TransactionService getTransactionService(); + + /** + * @return the namespace service (or null, if one is not provided) + */ + NamespaceService getNamespaceService(); + + /** + * @return the authentication service (or null, if one is not provided) + */ + AuthenticationService getAuthenticationService(); + + /** + * @return the node service (or null, if one is not provided) + */ + NodeService getNodeService(); + + /** + * @return the content service (or null, if one is not provided) + */ + ContentService getContentService(); + + /** + * @return the mimetype service (or null, if one is not provided) + */ + MimetypeService getMimetypeService(); + + /** + * @return the search service (or null, if one is not provided) + */ + SearchService getSearchService(); + + /** + * @return the version service (or null, if one is not provided) + */ + VersionService getVersionService(); + + /** + * @return the lock service (or null, if one is not provided) + */ + LockService getLockService(); + + /** + * @return the dictionary service (or null, if one is not provided) + */ + DictionaryService getDictionaryService(); + + /** + * @return the copy service (or null, if one is not provided) + */ + CopyService getCopyService(); + + /** + * @return the checkout / checkin service (or null, if one is not provided) + */ + CheckOutCheckInService getCheckOutCheckInService(); + + /** + * @return the category service (or null, if one is not provided) + */ + CategoryService getCategoryService(); + + /** + * @return the importer service or null if not present + */ + ImporterService getImporterService(); + + /** + * @return the exporter service or null if not present + */ + ExporterService getExporterService(); + + /** + * @return the rule service (or null, if one is not provided) + */ + RuleService getRuleService(); + + /** + * @return the action service (or null if one is not provided) + */ + ActionService getActionService(); + + /** + * @return the permission service (or null if one is not provided) + */ + PermissionService getPermissionService(); + + /** + * @return the authority service (or null if one is not provided) + */ + AuthorityService getAuthorityService(); + + /** + * @return the template service (or null if one is not provided) + */ + TemplateService getTemplateService(); + + /** + * @return the file-folder manipulation service (or null if one is not provided) + */ + FileFolderService getFileFolderService(); +} diff --git a/source/java/org/alfresco/service/cmr/action/Action.java b/source/java/org/alfresco/service/cmr/action/Action.java new file mode 100644 index 0000000000..d926a86bca --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/Action.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import java.util.Date; +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; + + +/** + * The rule action interface + * + * @author Roy Wetherall + */ +public interface Action extends ParameterizedItem +{ + /** + * Get the name of the action definition that relates to this action + * + * @return the action defintion name + */ + String getActionDefinitionName(); + + /** + * Get the title of the action + * + * @return the title of the action + */ + String getTitle(); + + /** + * Set the title of the action + * + * @param title the title of the action + */ + void setTitle(String title); + + /** + * Get the description of the action + * + * @return the description of the action + */ + String getDescription(); + + /** + * Set the description of the action + * + * @param description the description of the action + */ + void setDescription(String description); + + /** + * Get the node reference of the node that 'owns' this action. + *

    + * The node that 'owns' the action is th one that stores it via its + * actionable aspect association. + * + * @return node reference + */ + NodeRef getOwningNodeRef(); + + /** + * Gets a value indicating whether the action should be executed asychronously or not. + *

    + * The default is to execute the action synchronously. + * + * @return true if the action is executed asychronously, false otherwise. + */ + boolean getExecuteAsychronously(); + + /** + * Set the value that indicates whether the action should be executed asychronously or not. + * + * @param executeAsynchronously true if the action is to be executed asychronously, false otherwise. + */ + void setExecuteAsynchronously(boolean executeAsynchronously); + + /** + * Get the compensating action. + *

    + * This action is executed if the failure behaviour is to compensate and the action being executed + * fails. + * + * @return the compensating action + */ + Action getCompensatingAction(); + + /** + * Set the compensating action. + * + * @param action the compensating action + */ + void setCompensatingAction(Action action); + + /** + * Get the date the action was created + * + * @return action creation date + */ + Date getCreatedDate(); + + /** + * Get the name of the user that created the action + * + * @return user name + */ + String getCreator(); + + /** + * Get the date that the action was last modified + * + * @return aciton modification date + */ + Date getModifiedDate(); + + /** + * Get the name of the user that last modified the action + * + * @return user name + */ + String getModifier(); + + /** + * Indicates whether the action has any conditions specified + * + * @return true if the action has any conditions specified, flase otherwise + */ + boolean hasActionConditions(); + + /** + * Gets the index of an action condition + * + * @param actionCondition the action condition + * @return the index + */ + int indexOfActionCondition(ActionCondition actionCondition); + + /** + * Gets a list of the action conditions for this action + * + * @return list of action conditions + */ + List getActionConditions(); + + /** + * Get the action condition at a given index + * + * @param index the index + * @return the action condition + */ + ActionCondition getActionCondition(int index); + + /** + * Add an action condition to the action + * + * @param actionCondition an action condition + */ + void addActionCondition(ActionCondition actionCondition); + + /** + * Add an action condition at the given index + * + * @param index the index + * @param actionCondition the action condition + */ + void addActionCondition(int index, ActionCondition actionCondition); + + /** + * Replaces the current action condition at the given index with the + * action condition provided. + * + * @param index the index + * @param actionCondition the action condition + */ + void setActionCondition(int index, ActionCondition actionCondition); + + /** + * Removes an action condition + * + * @param actionCondition an action condition + */ + void removeActionCondition(ActionCondition actionCondition); + + /** + * Removes all action conditions + */ + void removeAllActionConditions(); +} diff --git a/source/java/org/alfresco/service/cmr/action/ActionCondition.java b/source/java/org/alfresco/service/cmr/action/ActionCondition.java new file mode 100644 index 0000000000..624dc9c4ee --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ActionCondition.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + + +/** + * Rule condition interface + * + * @author Roy Wetherall + */ +public interface ActionCondition extends ParameterizedItem +{ + /** + * Get the action condition definition name + * + * @param the action condition definition name + */ + public String getActionConditionDefinitionName(); + + /** + * Set whether the condition result should be inverted. + *

    + * This is achieved by applying the NOT logical operator to the + * result. + *

    + * The default value is false. + * + * @param invertCondition true indicates that the result of the condition + * is inverted, false otherwise. + */ + public void setInvertCondition(boolean invertCondition); + + /** + * Indicates whether the condition result should be inverted. + *

    + * This is achieved by applying the NOT logical operator to the result. + *

    + * The default value is false. + * + * @return true indicates that the result of the condition is inverted, false + * otherwise + */ + public boolean getInvertCondition(); +} diff --git a/source/java/org/alfresco/service/cmr/action/ActionConditionDefinition.java b/source/java/org/alfresco/service/cmr/action/ActionConditionDefinition.java new file mode 100644 index 0000000000..f7110fac9f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ActionConditionDefinition.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + + + +/** + * Rule condition interface + * + * @author Roy Wetherall + */ +public interface ActionConditionDefinition extends ParameterizedItemDefinition +{ + +} diff --git a/source/java/org/alfresco/service/cmr/action/ActionDefinition.java b/source/java/org/alfresco/service/cmr/action/ActionDefinition.java new file mode 100644 index 0000000000..0c19c2b975 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ActionDefinition.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + + + +/** + * Rule action interface. + * + * @author Roy Wetherall + */ +public interface ActionDefinition extends ParameterizedItemDefinition +{ + +} diff --git a/source/java/org/alfresco/service/cmr/action/ActionExecutionStatus.java b/source/java/org/alfresco/service/cmr/action/ActionExecutionStatus.java new file mode 100644 index 0000000000..e2f03a433b --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ActionExecutionStatus.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import java.io.Serializable; + +/** + * Action execution status enumeration + * + * @author Roy Wetherall + */ +public enum ActionExecutionStatus implements Serializable +{ + PENDING, // The action is queued pending execution + RUNNING, // The action is currently executing + SUCCEEDED, // The action has completed successfully + FAILED, // The action has failed + COMPENSATED // The action has failed and a compensating action has been been queued for execution +} diff --git a/source/java/org/alfresco/service/cmr/action/ActionService.java b/source/java/org/alfresco/service/cmr/action/ActionService.java new file mode 100644 index 0000000000..7d2605c677 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ActionService.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Action service interface + * + * @author Roy Wetherall + */ +public interface ActionService +{ + /** + * Get a named action definition + * + * @param name the name of the action definition + * @return the action definition + */ + ActionDefinition getActionDefinition(String name); + + /** + * Get all the action definitions + * + * @return the list action definitions + */ + List getActionDefinitions(); + + /** + * Get a named action condition definition + * + * @param name the name of the action condition definition + * @return the action condition definition + */ + ActionConditionDefinition getActionConditionDefinition(String name); + + /** + * Get all the action condition definitions + * + * @return the list of aciton condition definitions + */ + List getActionConditionDefinitions(); + + /** + * Create a new action + * + * @param name the action definition name + * @return the action + */ + Action createAction(String name); + + /** + * Create a new action specifying the initial set of parameter values + * + * @param name the action defintion name + * @param params the parameter values + * @return the action + */ + Action createAction(String name, Map params); + + /** + * Create a composite action + * + * @return the composite action + */ + CompositeAction createCompositeAction(); + + /** + * Create an action condition + * + * @param name the action condition definition name + * @return the action condition + */ + ActionCondition createActionCondition(String name); + + /** + * Create an action condition specifying the initial set of parameter values + * + * @param name the aciton condition definition name + * @param params the parameter valeus + * @return the action condition + */ + ActionCondition createActionCondition(String name, Map params); + + /** + * The actions conditions are always checked. + * + * @see ActionService#executeAction(Action, NodeRef, boolean) + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + */ + void executeAction(Action action, NodeRef actionedUponNodeRef); + + /** + * The action is sexecuted based on the asynchronous attribute of the action. + * + * @see ActionService#executeAction(Action, NodeRef, boolean, boolean) + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + * @param checkConditions indicates whether the conditions should be checked + */ + void executeAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions); + + /** + * Executes the specified action upon the node reference provided. + *

    + * If specified that the conditions should be checked then any conditions + * set on the action are evaluated. + *

    + * If the conditions fail then the action is not executed. + *

    + * If an action has no conditions then the action will always be executed. + *

    + * If the conditions are not checked then the action will always be executed. + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + * @param checkConditions indicates whether the conditions should be checked before + * executing the action + * @param executeAsynchronously indicates whether the action should be executed asychronously or not, this value overrides + * the value set on the action its self + */ + void executeAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions, boolean executeAsynchronously); + + /** + * Evaluted the conditions set on an action. + *

    + * Returns true if the action has no conditions. + *

    + * If the action has more than one condition their results are combined using the 'AND' + * logical operator. + * + * @param action the action + * @param actionedUponNodeRef the actioned upon node reference + * @return true if the condition succeeds, false otherwise + */ + boolean evaluateAction(Action action, NodeRef actionedUponNodeRef); + + /** + * Evaluate an action condition. + * + * @param condition the action condition + * @param actionedUponNodeRef the actioned upon node reference + * @return true if the condition succeeds, false otherwise + */ + boolean evaluateActionCondition(ActionCondition condition, NodeRef actionedUponNodeRef); + + /** + * Save an action against a node reference. + *

    + * The node will be made configurable if it is not already. + *

    + * If the action already exists then its details will be updated. + * + * @param nodeRef the node reference + * @param action the action + */ + void saveAction(NodeRef nodeRef, Action action); + + /** + * Gets all the actions currently saved on the given node reference. + * + * @param nodeRef the ndoe reference + * @return the list of actions + */ + List getActions(NodeRef nodeRef); + + /** + * Gets an action stored against a given node reference. + *

    + * Returns null if the action can not be found. + * + * @param nodeRef the node reference + * @param actionId the action id + * @return the action + */ + Action getAction(NodeRef nodeRef, String actionId); + + /** + * Removes an action associatied with a node reference. + * + * @param nodeRef the node reference + * @param action the action + */ + void removeAction(NodeRef nodeRef, Action action); + + /** + * Removes all actions associated with a node reference + * + * @param nodeRef the node reference + */ + void removeAllActions(NodeRef nodeRef); + +} diff --git a/source/java/org/alfresco/service/cmr/action/ActionServiceException.java b/source/java/org/alfresco/service/cmr/action/ActionServiceException.java new file mode 100644 index 0000000000..e681254ec4 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ActionServiceException.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Rule Service Exception Class + * + * @author Roy Wetherall + */ +public class ActionServiceException extends AlfrescoRuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3257571685241467958L; + + public ActionServiceException(String msgId) + { + super(msgId); + } + + public ActionServiceException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + public ActionServiceException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } + + public ActionServiceException(String msgId, Throwable cause) + { + super(msgId, cause); + } +} diff --git a/source/java/org/alfresco/service/cmr/action/CompositeAction.java b/source/java/org/alfresco/service/cmr/action/CompositeAction.java new file mode 100644 index 0000000000..2d13319b60 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/CompositeAction.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import java.util.List; + +/** + * Composite action + * + * @author Roy Wetherall + */ +public interface CompositeAction extends Action +{ + /** + * Indicates whether there are any actions + * + * @return true if there are actions, false otherwise + */ + boolean hasActions(); + + /** + * Add an action to the end of the list + * + * @param action the action + */ + void addAction(Action action); + + /** + * Add an action to the list at the index specified + * + * @param index the index + * @param action the action + */ + void addAction(int index, Action action); + + /** + * Replace the action at the specfied index with the passed action. + * + * @param index the index + * @param action the action + */ + void setAction(int index, Action action); + + /** + * Gets the index of an action + * + * @param action the action + * @return the index + */ + int indexOfAction(Action action); + + /** + * Get list containing the actions in their current order + * + * @return the list of actions + */ + List getActions(); + + /** + * Get an action at a given index + * + * @param index the index + * @return the action + */ + Action getAction(int index); + + /** + * Remove an action from the list + * + * @param action the action + */ + void removeAction(Action action); + + /** + * Remove all actions from the list + */ + void removeAllActions(); +} diff --git a/source/java/org/alfresco/service/cmr/action/ParameterDefinition.java b/source/java/org/alfresco/service/cmr/action/ParameterDefinition.java new file mode 100644 index 0000000000..53e822b170 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ParameterDefinition.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import org.alfresco.service.namespace.QName; + +/** + * Parameter definition interface. + * + * @author Roy Wetherall + */ +public interface ParameterDefinition +{ + /** + * Get the name of the parameter. + *

    + * This is unique and is used to identify the parameter. + * + * @return the parameter name + */ + public String getName(); + + /** + * Get the type of parameter + * + * @return the parameter type qname + */ + public QName getType(); + + /** + * Indicates whether the parameter is mandatory or not. + *

    + * If a parameter is mandatory it means that the value can not be null. + * + * @return true if the parameter is mandatory, false otherwise + */ + public boolean isMandatory(); + + /** + * Get the display label of the parameter. + * + * @return the parameter display label + */ + public String getDisplayLabel(); + +} diff --git a/source/java/org/alfresco/service/cmr/action/ParameterizedItem.java b/source/java/org/alfresco/service/cmr/action/ParameterizedItem.java new file mode 100644 index 0000000000..853a5d5103 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ParameterizedItem.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import java.io.Serializable; +import java.util.Map; + +/** + * Rule item interface + * + * @author Roy Wetherall + */ +public interface ParameterizedItem +{ + /** + * Unique identifier for the parameterized item + * + * @return the id string + */ + public String getId(); + + /** + * Get the parameter values + * + * @return get the parameter values + */ + public Map getParameterValues(); + + /** + * Get value of a named parameter. + * + * @param name the parameter name + * @return the value of the parameter + */ + public Serializable getParameterValue(String name); + + /** + * Sets the parameter values + * + * @param parameterValues the parameter values + */ + public void setParameterValues( + Map parameterValues); + + /** + * Sets the value of a parameter. + * + * @param name the parameter name + * @param value the parameter value + */ + public void setParameterValue(String name, Serializable value); +} diff --git a/source/java/org/alfresco/service/cmr/action/ParameterizedItemDefinition.java b/source/java/org/alfresco/service/cmr/action/ParameterizedItemDefinition.java new file mode 100644 index 0000000000..b3093c42f7 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/action/ParameterizedItemDefinition.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.action; + +import java.util.List; + +public interface ParameterizedItemDefinition +{ + /** + * Get the name of the rule item. + *

    + * The name is unique and is used to identify the rule item. + * + * @return the name of the rule action + */ + public String getName(); + + /** + * The title of the parameterized item definition + * + * @return the title + */ + public String getTitle(); + + /** + * The description of the parameterized item definition + * + * @return the description + */ + public String getDescription(); + + /** + * Indicates whether the parameterized item allows adhoc properties to be set + * + * @return true if ashoc properties are allowed, false otherwise + */ + public boolean getAdhocPropertiesAllowed(); + + /** + * A list containing the parmameter defintions for this rule item. + * + * @return a list of parameter definitions + */ + public List getParameterDefinitions(); + + /** + * Get the parameter definition by name + * + * @param name the name of the parameter + * @return the parameter definition, null if none found + */ + public ParameterDefinition getParameterDefintion(String name); +} diff --git a/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java b/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java new file mode 100644 index 0000000000..5539eac729 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.coci; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Version operations service interface + * + * @author Roy Wetherall + */ +public interface CheckOutCheckInService +{ + /** + * Checks out the given node placing a working copy in the destination specified. + *

    + * When a node is checked out a read-only lock is placed on the origional node and + * a working copy is placed in the destination specified. + *

    + * The copy aspect is applied to the working copy so that the origional node can be + * identified. + *

    + * The working copy aspect is applied to the working copy so that it can be identified + * as the working copy of a checked out node. + *

    + * The working copy node reference is returned to the caller. + * + * @param nodeRef a reference to the node to checkout + * @param destinationParentNodeRef the destination node reference for the working + * copy + * @param destinationAssocTypeQName the destination child assoc type for the working + * copy + * @param destinationAssocQName the destination child assoc qualified name for + * the working copy + * @return node reference to the created working copy + */ + public NodeRef checkout( + NodeRef nodeRef, + NodeRef destinationParentNodeRef, + QName destinationAssocTypeQName, + QName destinationAssocQName); + + /** + * Checks out the working copy of the node into the same parent node with the same child + * associations details. + * + * @see CheckOutCheckInService#checkout(NodeRef, NodeRef, QName, QName) + * + * @param nodeRef a reference to the node to checkout + * @return a node reference to the created working copy + */ + public NodeRef checkout(NodeRef nodeRef); + + /** + * Checks in the working node specified. + *

    + * When a working copy is checked in the current state of the working copy is copyied to the + * origional node. This will include any content updated in the working node. + *

    + * If version properties are provided the origional node will be versioned and updated accordingly. + *

    + * If a content Url is provided it will be used to update the content of the working node before the + * checkin opertaion takes place. + *

    + * Once the operation has completed the read lock applied to the origional node during checkout will + * be removed and the working copy of the node deleted from the repository, unless the operation is + * instructed to keep the origional node checked out. In which case the lock and the working copy will + * remain. + *

    + * The node reference to the origional node is returned. + * + * @param workingCopyNodeRef the working copy node reference + * @param versionProperties the version properties. If null is passed then the origional node + * is NOT versioned during the checkin operation. + * @param contentUrl a content url that should be set on the working copy before + * the checkin opertation takes place. If null then the current working + * copy content is copied back to the origional node. + * @param keepCheckedOut indicates whether the node should remain checked out after the checkin + * has taken place. When the node remains checked out the working node + * reference remains the same. + * @return the node reference to the origional node, updated with the checked in + * state + */ + public NodeRef checkin( + NodeRef workingCopyNodeRef, + Map versionProperties, + String contentUrl, + boolean keepCheckedOut); + + /** + * By default the checked in node is not keep checked in. + * + * @see VersionOperationsService#checkin(NodeRef, HashMap, String, boolean) + * + * @param workingCopyNodeRef the working copy node reference + * @param versionProperties the version properties. If null is passed then the origional node + * is NOT versioned during the checkin operation. + * @param contentUrl a content url that should be set on the working copy before + * the checkin opertation takes place. If null then the current working + * copy content is copied back to the origional node. + * @return the node reference to the origional node, updated with the checked in + * state + */ + public NodeRef checkin( + NodeRef workingCopyNodeRef, + Map versionProperties, + String contentUrl); + + /** + * If no content url is specified then current content set on the working + * copy is understood to be current. + * + * @see VersionOperationsService#checkin(NodeRef, HashMap, String) + * + * @param workingCopyNodeRef the working copy node reference + * @param versionProperties the version properties. If null is passed then the origional node + * is NOT versioned during the checkin operation. + * @return the node reference to the origional node, updated with the checked in + * state + */ + public NodeRef checkin( + NodeRef workingCopyNodeRef, + Map versionProperties); + + /** + * Cancels the checkout for a given working copy. + *

    + * The read-only lock on the origional node is removed and the working copy is removed. + *

    + * Note that all modification made to the working copy will be lost and the origional node + * will remiain unchanged. + *

    + * A reference to the origional node reference is returned. + * + * @param workingCopyNodeRef the working copy node reference + * @return the origional node reference + */ + public NodeRef cancelCheckout(NodeRef workingCopyNodeRef); + + /** + * Helper method to retrieve the working copy node reference for a checked out node. + *

    + * A null node reference is returned if the node is not checked out. + * + * @param nodeRef a node reference + * @return the working copy node reference or null if none. + */ + public NodeRef getWorkingCopy(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInServiceException.java b/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInServiceException.java new file mode 100644 index 0000000000..df94d7728d --- /dev/null +++ b/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInServiceException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.coci; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Version opertaions service exception class + * + * @author Roy Wetherall + */ +public class CheckOutCheckInServiceException extends AlfrescoRuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3258410621186618417L; + + /** + * Constructor + * + * @param message the error message + */ + public CheckOutCheckInServiceException(String message) + { + super(message); + } + + /** + * Constructor + * + * @param message the error message + * @param throwable the cause of the exeption + */ + public CheckOutCheckInServiceException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/AspectDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/AspectDefinition.java new file mode 100644 index 0000000000..e31cd3d720 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/AspectDefinition.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + + +/** + * Read-only definition of an Aspect. + * + * @author David Caruana + */ +public interface AspectDefinition extends ClassDefinition +{ + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java new file mode 100644 index 0000000000..e6178bdc83 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import org.alfresco.service.namespace.QName; + + +/** + * Read-only definition of an Association. + * + * @author David Caruana + * + */ +public interface AssociationDefinition +{ + + /** + * @return defining model + */ + public ModelDefinition getModel(); + + /** + * @return the qualified name + */ + public QName getName(); + + /** + * @return the human-readable title + */ + public String getTitle(); + + /** + * @return the human-readable description + */ + public String getDescription(); + + /** + * Is this a child association? + * + * @return true => child, false => general relationship + */ + public boolean isChild(); + + /** + * Is this association maintained by the Repository? + * + * @return true => system maintained, false => client may maintain + */ + public boolean isProtected(); + + /** + * @return the source class + */ + public ClassDefinition getSourceClass(); + + /** + * @return the role of the source class in this association? + */ + public QName getSourceRoleName(); + + /** + * Is the source class optional in this association? + * + * @return true => cardinality > 0 + */ + public boolean isSourceMandatory(); + + /** + * Can there be many source class instances in this association? + * + * @return true => cardinality > 1, false => cardinality of 0 or 1 + */ + public boolean isSourceMany(); + + /** + * @return the target class + */ + public ClassDefinition getTargetClass(); + + /** + * @return the role of the target class in this association? + */ + public QName getTargetRoleName(); + + /** + * Is the target class optional in this association? + * + * @return true => cardinality > 0 + */ + public boolean isTargetMandatory(); + + /** + * Can there be many target class instances in this association? + * + * @return true => cardinality > 1, false => cardinality of 0 or 1 + */ + public boolean isTargetMany(); + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/ChildAssociationDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/ChildAssociationDefinition.java new file mode 100644 index 0000000000..e7c3658037 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/ChildAssociationDefinition.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +/** + * Read-only definition of a Child Association. + * + * @author David Caruana + * + */ +public interface ChildAssociationDefinition extends AssociationDefinition +{ + + /** + * @return the required name of children (or null if none) + */ + public String getRequiredChildName(); + + /** + * @return whether duplicate child names allowed within this association? + */ + public boolean getDuplicateChildNamesAllowed(); + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/ClassDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/ClassDefinition.java new file mode 100644 index 0000000000..b209bb48f4 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/ClassDefinition.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.namespace.QName; + +/** + * Read-only definition of a Class. + * + * @author David Caruana + */ +public interface ClassDefinition +{ + /** + * @return defining model + */ + public ModelDefinition getModel(); + + /** + * @return the qualified name of the class + */ + public QName getName(); + + /** + * @return the human-readable class title + */ + public String getTitle(); + + /** + * @return the human-readable class description + */ + public String getDescription(); + + /** + * @return the super class (or null, if this is the root) + */ + public QName getParentName(); + + /** + * @return true => aspect, false => type + */ + public boolean isAspect(); + + /** + * @return the properties of the class, including inherited properties + */ + public Map getProperties(); + + /** + * @return a map containing the default property values, including inherited properties + */ + public Map getDefaultValues(); + + /** + * Fetch all associations for which this is a source type, including child associations. + * + * @return the associations including inherited ones + * @see ChildAssociationDefinition + */ + public Map getAssociations(); + + /** + * @return true => this class supports child associations + */ + public boolean isContainer(); + + /** + * Fetch only child associations for which this is a source type. + * + * @return all child associations applicable to this type, including those + * inherited from super types + */ + public Map getChildAssociations(); + + /** + * Fetch all associations for which this is a target type, including child associations. + * + * @return the associations including inherited ones + */ + // TODO: public Map getTargetAssociations(); + + /** + * @return the default aspects associated with this type + */ + public List getDefaultAspects(); + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/DataTypeDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/DataTypeDefinition.java new file mode 100644 index 0000000000..b2d48833be --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/DataTypeDefinition.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import java.util.Locale; + +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + + +/** + * Read-only definition of a Data Type + * + * @author David Caruana + */ +public interface DataTypeDefinition +{ + // + // Built-in Property Types + // + public QName ANY = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "any"); + public QName TEXT = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "text"); + public QName CONTENT = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "content"); + public QName INT = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "int"); + public QName LONG = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "long"); + public QName FLOAT = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "float"); + public QName DOUBLE = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "double"); + public QName DATE = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "date"); + public QName DATETIME = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "datetime"); + public QName BOOLEAN = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "boolean"); + public QName QNAME = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "qname"); + public QName CATEGORY = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "category"); + public QName NODE_REF = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "noderef"); + public QName PATH = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "path"); + + + /** + * @return defining model + */ + public ModelDefinition getModel(); + + /** + * @return the qualified name of the data type + */ + public QName getName(); + + /** + * @return the human-readable class title + */ + public String getTitle(); + + /** + * @return the human-readable class description + */ + public String getDescription(); + + /** + * @return the indexing analyser class + */ + public String getAnalyserClassName(); + + /** + * @return the indexing analyser class for the specified locale + */ + public String getAnalyserClassName(Locale locale); + + /** + * @return the equivalent java class name (or null, if not mapped) + */ + public String getJavaClassName(); + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/DictionaryException.java b/source/java/org/alfresco/service/cmr/dictionary/DictionaryException.java new file mode 100644 index 0000000000..fb62d706cd --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/DictionaryException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + + +/** + * Base Exception of Data Dictionary Exceptions. + * + * @author David Caruana + */ +public class DictionaryException extends RuntimeException +{ + private static final long serialVersionUID = 3257008761007847733L; + + public DictionaryException(String msg) + { + super(msg); + } + + public DictionaryException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/DictionaryService.java b/source/java/org/alfresco/service/cmr/dictionary/DictionaryService.java new file mode 100644 index 0000000000..5ad1b5fdda --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/DictionaryService.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import java.util.Collection; + +import org.alfresco.service.namespace.QName; + + +/** + * This interface represents the Repository Data Dictionary. The + * dictionary provides access to content meta-data such as Type + * and Aspect descriptions. + * + * Content meta-data is organised into models where each model is + * given a qualified name. This means that it is safe to develop + * independent models and bring them together into the same + * Repository without name clashes (as long their namespace is + * different). + * + * @author David Caruana + */ +public interface DictionaryService +{ + + /** + * @return the names of all models that have been registered with the Repository + */ + public Collection getAllModels(); + + /** + * @param model the model name to retrieve + * @return the specified model (or null, if it doesn't exist) + */ + public ModelDefinition getModel(QName model); + + /** + * @return the names of all data types that have been registered with the Repository + */ + Collection getAllDataTypes(); + + /** + * @param model the model to retrieve data types for + * @return the names of all data types defined within the specified model + */ + Collection getDataTypes(QName model); + + /** + * @param name the name of the data type to retrieve + * @return the data type definition (or null, if it doesn't exist) + */ + DataTypeDefinition getDataType(QName name); + + /** + * @param javaClass java class to find datatype for + * @return the data type definition (or null, if a mapping does not exist) + */ + DataTypeDefinition getDataType(Class javaClass); + + /** + * @return the names of all types that have been registered with the Repository + */ + Collection getAllTypes(); + + /** + * @param model the model to retrieve types for + * @return the names of all types defined within the specified model + */ + Collection getTypes(QName model); + + /** + * @param name the name of the type to retrieve + * @return the type definition (or null, if it doesn't exist) + */ + TypeDefinition getType(QName name); + + /** + * Construct an anonymous type that combines the definitions of the specified + * type and aspects. + * + * @param type the type to start with + * @param aspects the aspects to combine with the type + * @return the anonymous type definition + */ + TypeDefinition getAnonymousType(QName type, Collection aspects); + + /** + * @return the names of all aspects that have been registered with the Repository + */ + Collection getAllAspects(); + + /** + * @param model the model to retrieve aspects for + * @return the names of all aspects defined within the specified model + */ + Collection getAspects(QName model); + + /** + * @param name the name of the aspect to retrieve + * @return the aspect definition (or null, if it doesn't exist) + */ + AspectDefinition getAspect(QName name); + + /** + * @param name the name of the class (type or aspect) to retrieve + * @return the class definition (or null, if it doesn't exist) + */ + ClassDefinition getClass(QName name); + + /** + * Determines whether a class is a sub-class of another class + * + * @param className the sub-class to test + * @param ofClassName the class to test against + * @return true => the class is a sub-class (or itself) + */ + boolean isSubClass(QName className, QName ofClassName); + + /** + * Gets the definition of the property as defined by the specified Class. + * + * Note: A sub-class may override the definition of a property that's + * defined in a super-class. + * + * @param className the class name + * @param propertyName the property name + * @return the property definition (or null, if it doesn't exist) + */ + PropertyDefinition getProperty(QName className, QName propertyName); + + /** + * Gets the definition of the property as defined by its owning Class. + * + * @param propertyName the property name + * @return the property definition (or null, if it doesn't exist) + */ + PropertyDefinition getProperty(QName propertyName); + + /** + * Gets the definition of the association as defined by its owning Class. + * + * @param associationName the property name + * @return the association definition (or null, if it doesn't exist) + */ + AssociationDefinition getAssociation(QName associationName); + + // TODO: Behaviour definitions + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/InvalidAspectException.java b/source/java/org/alfresco/service/cmr/dictionary/InvalidAspectException.java new file mode 100644 index 0000000000..373dc25eae --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/InvalidAspectException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import org.alfresco.service.namespace.QName; + +/** + * Thrown when a reference to an aspect is incorrect. + * + * @author Derek Hulley + */ +public class InvalidAspectException extends InvalidClassException +{ + private static final long serialVersionUID = 3257290240330051893L; + + public InvalidAspectException(QName aspectName) + { + super(null, aspectName); + } + + public InvalidAspectException(String msg, QName aspectName) + { + super(msg, aspectName); + } + + /** + * @return Returns the offending aspect name + */ + public QName getAspectName() + { + return getClassName(); + } +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/InvalidClassException.java b/source/java/org/alfresco/service/cmr/dictionary/InvalidClassException.java new file mode 100644 index 0000000000..1a2506f0dc --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/InvalidClassException.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.namespace.QName; + +/** + * Thrown when an operation cannot be performed because the dictionary class + * reference does not exist. + * + */ +public class InvalidClassException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 3256722870754293558L; + + private QName className; + + public InvalidClassException(QName className) + { + this(null, className); + } + + public InvalidClassException(String msg, QName className) + { + super(msg); + this.className = className; + } + + /** + * @return Returns the offending class name + */ + public QName getClassName() + { + return className; + } +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/InvalidTypeException.java b/source/java/org/alfresco/service/cmr/dictionary/InvalidTypeException.java new file mode 100644 index 0000000000..0c433fa075 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/InvalidTypeException.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import org.alfresco.service.namespace.QName; + +/** + * Thrown when an operation cannot be performed because a type is not recognised + * by the data dictionary + * + * @author Derek Hulley + */ +public class InvalidTypeException extends InvalidClassException +{ + private static final long serialVersionUID = 3256722870754293558L; + + public InvalidTypeException(QName typeName) + { + super(null, typeName); + } + + public InvalidTypeException(String msg, QName typeName) + { + super(msg, typeName); + } + + /** + * @return Returns the offending type name + */ + public QName getTypeName() + { + return getClassName(); + } +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/ModelDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/ModelDefinition.java new file mode 100644 index 0000000000..9ca6819acf --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/ModelDefinition.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import java.util.Date; + +import org.alfresco.service.namespace.QName; + + +/** + * Read-only definition of a Model. + * + * @author David Caruana + */ +public interface ModelDefinition +{ + /** + * @return the model name + */ + public QName getName(); + + /** + * @return the model description + */ + public String getDescription(); + + /** + * @return the model author + */ + public String getAuthor(); + + /** + * @return the date when the model was published + */ + public Date getPublishedDate(); + + /** + * @return the model version + */ + public String getVersion(); + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/PropertyDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/PropertyDefinition.java new file mode 100644 index 0000000000..af519bf2f8 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/PropertyDefinition.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + +import org.alfresco.service.namespace.QName; + +/** + * Read-only definition of a Property. + * + * @author David Caruana + */ +public interface PropertyDefinition +{ + /** + * @return defining model + */ + public ModelDefinition getModel(); + + /** + * @return the qualified name of the property + */ + public QName getName(); + + /** + * @return the human-readable class title + */ + public String getTitle(); + + /** + * @return the human-readable class description + */ + public String getDescription(); + + /** + * @return the default value + */ + public String getDefaultValue(); + + /** + * @return the qualified name of the property type + */ + public DataTypeDefinition getDataType(); + + /** + * @return Returns the owning class's defintion + */ + public ClassDefinition getContainerClass(); + + /** + * @return true => multi-valued, false => single-valued + */ + public boolean isMultiValued(); + + /** + * @return true => mandatory, false => optional + */ + public boolean isMandatory(); + + /** + * @return true => system maintained, false => client may maintain + */ + public boolean isProtected(); + + /** + * @return true => indexed, false => not indexed + */ + public boolean isIndexed(); + + /** + * @return true => stored in index + */ + public boolean isStoredInIndex(); + + /** + * @return true => tokenised when it is indexed (the stored value will not be tokenised) + */ + public boolean isTokenisedInIndex(); + + /** + * All non atomic properties will be indexed at the same time. + * + * @return true => The attribute must be indexed in the commit of the transaction. + * false => the indexing will be done in the background and may be out of date. + */ + public boolean isIndexedAtomically(); + +} diff --git a/source/java/org/alfresco/service/cmr/dictionary/TypeDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/TypeDefinition.java new file mode 100644 index 0000000000..9fde1f2df5 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/dictionary/TypeDefinition.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.dictionary; + + +/** + * Read-only definition of a Type + * + * @author David Caruana + */ +public interface TypeDefinition extends ClassDefinition +{ + + +} diff --git a/source/java/org/alfresco/service/cmr/lock/LockService.java b/source/java/org/alfresco/service/cmr/lock/LockService.java new file mode 100644 index 0000000000..59c4aa47f7 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/lock/LockService.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.lock; + +import java.util.Collection; +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; + + +/** + * Interface for public and internal lock operations. + * + * @author Roy Wetherall + */ +public interface LockService +{ + /** + * Places a lock on a node. + *

    + * The lock prevents any other user or process from comitting updates + * to the node untill the lock is released. + *

    + * The user reference passed indicates who the owner of the lock is. + *

    + * A lock made with this call will never expire. + * + * @param nodeRef a reference to a node + * @param userName a reference to the user that will own the lock + * @param lockType the lock type + * @throws UnableToAquireLockException + * thrown if the lock could not be obtained + */ + public void lock(NodeRef nodeRef, LockType lockType) + throws UnableToAquireLockException; + + /** + * Places a lock on a node. + *

    + * The lock prevents any other user or process from comitting updates + * to the node untill the lock is released. + *

    + * The user reference passed indicates who the owner of the lock is. + *

    + * If the time to expire is 0 then the lock will never expire. Otherwise the + * timeToExpire indicates the number of seconds before the lock expires. When + * a lock expires the lock is considered to have been released. + *

    + * If the node is already locked and the user is the lock owner then the lock will + * be renewed with the passed timeToExpire. + * + * @param nodeRef a reference to a node + * @param userName a reference to the user that will own the lock + * @param lockType the lock type + * @param timeToExpire the number of seconds before the locks expires. + * @throws UnableToAquireLockException + * thrown if the lock could not be obtained + */ + public void lock(NodeRef nodeRef, LockType lockType, int timeToExpire) + throws UnableToAquireLockException; + + /** + * Places a lock on a node and optionally on all its children. + *

    + * The lock prevents any other user or process from comitting updates + * to the node untill the lock is released. + *

    + * The user reference passed indicates who the owner of the lock(s) is. + * If any one of the child locks can not be taken then an exception will + * be raised and all locks canceled. + *

    + * If the time to expire is 0 then the lock will never expire. Otherwise the + * timeToExpire indicates the number of seconds before the lock expires. When + * a lock expires the lock is considered to have been released. + *

    + * If the node is already locked and the user is the lock owner then the lock will + * be renewed with the passed timeToExpire. + * + * @param nodeRef a reference to a node + * @param userName a reference to the user that will own the lock(s) + * @param lockType the lock type + * @param timeToExpire the number of seconds before the locks expires. + * @param lockChildren if true indicates that all the children (and + * grandchildren, etc) of the node will also be locked, + * false otherwise + * + * @throws UnableToAquireLockException + * thrown if the lock could not be obtained + */ + public void lock(NodeRef nodeRef, LockType lockType, int timeToExpire, boolean lockChildren) + throws UnableToAquireLockException; + + /** + * Places a lock on all the nodes referenced in the passed list. + *

    + * The lock prevents any other user or process from comitting updates + * to the node untill the lock is released. + *

    + * The user reference passed indicates who the owner of the lock(s) is. + * If any one of the child locks can not be taken then an exception will + * be raised and all locks canceled. + *

    + * If the time to expire is 0 then the lock will never expire. Otherwise the + * timeToExpire indicates the number of seconds before the lock expires. When + * a lock expires the lock is considered to have been released. + *

    + * If the node is already locked and the user is the lock owner then the lock will + * be renewed with the passed timeToExpire. + * + * @param nodeRefs a list of node references + * @param userName a reference to the user that will own the lock(s) + * @param lockType the type of lock being created + * @param timeToExpire the number of seconds before the locks expires. + * @throws UnableToAquireLockException + * thrown if the lock could not be obtained + */ + public void lock(Collection nodeRefs, LockType lockType, int timeToExpire) + throws UnableToAquireLockException; + + /** + * Removes the lock on a node. + *

    + * The user must have sufficient permissions to remove the lock (ie: be the + * owner of the lock or have admin rights) otherwise an exception will be raised. + * + * @param nodeRef a reference to a node + * @param userName the user reference + * @throws UnableToReleaseLockException + * thrown if the lock could not be released + */ + public void unlock(NodeRef nodeRef) + throws UnableToReleaseLockException; + + /** + * Removes the lock on a node and optional on its children. + *

    + * The user must have sufficient permissions to remove the lock(s) (ie: be + * the owner of the lock(s) or have admin rights) otherwise an exception + * will be raised. + *

    + * If one of the child nodes is not locked then it will be ignored and + * the process continue without error. + *

    + * If the lock on any one of the child nodes cannot be released then an + * exception will be raised. + * + * @param nodeRef a node reference + * @param userName the user reference + * @param lockChildren if true then all the children (and grandchildren, etc) + * of the node will also be unlocked, false otherwise + * @throws UnableToReleaseLockException + * thrown if the lock could not be released + */ + public void unlock(NodeRef nodeRef, boolean lockChildren) + throws UnableToReleaseLockException; + + /** + * Removes a lock on the nodes provided. + *

    + * The user must have sufficient permissions to remove the locks (ie: be + * the owner of the locks or have admin rights) otherwise an exception + * will be raised. + *

    + * If one of the nodes is not locked then it will be ignored and the + * process will continue without an error. + *

    + * If the lock on any one of the nodes cannot be released than an exception + * will be raised and the process rolled back. + * + * @param nodeRefs the node references + * @param userName the user reference + * @throws UnableToReleaseLockException + * thrown if the lock could not be released + */ + public void unlock(Collection nodeRefs) + throws UnableToReleaseLockException; + + /** + * Gets the lock status for the node reference relative to the current user. + * + * @see LockService#getLockStatus(NodeRef, NodeRef) + * + * @param nodeRef the node reference + * @return the lock status + */ + public LockStatus getLockStatus(NodeRef nodeRef); + + /** + * Gets the lock type for the node indicated. + *

    + * Returns null if the node is not locked. + *

    + * Throws an exception if the node does not have the lock aspect. + * + * @param nodeRef the node reference + * @return the lock type, null is returned if the object in question has no + * lock + */ + public LockType getLockType(NodeRef nodeRef); + + /** + * Checks to see if the node is locked or not. Gets the user reference from the current + * session. + *

    + * Throws a NodeLockedException based on the lock status of the lock, the user ref and the + * lock type. + * + * @param nodeRef the node reference + */ + public void checkForLock(NodeRef nodeRef); + + /** + * Get all the node references that the current user has locked. + * + * @param storeRef the store reference + * @return a list of nodes that the current user has locked. + */ + public List getLocks(StoreRef storeRef); + + /** + * Get all the node references that the current user has locked filtered by the provided lock type. + * + * @param storeRef the store reference + * @param lockType the lock type to filter the results by + * + * @return a list of nodes that the current user has locked filtered by the lock type provided + */ + public List getLocks(StoreRef storeRef, LockType lockType); +} diff --git a/source/java/org/alfresco/service/cmr/lock/LockStatus.java b/source/java/org/alfresco/service/cmr/lock/LockStatus.java new file mode 100644 index 0000000000..ba5cdf2810 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/lock/LockStatus.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.lock; + +/** + * Enum used to indicate lock status. + * + * @author Roy Wetherall + */ +public enum LockStatus +{ + NO_LOCK, // Indicates that there is no lock present + LOCKED, // Indicates that the node is locked + LOCK_OWNER, // Indicates that the node is locked and you have lock ownership rights + LOCK_EXPIRED // Indicates that the lock has expired and the node can be considered to be unlocked +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/lock/LockType.java b/source/java/org/alfresco/service/cmr/lock/LockType.java new file mode 100644 index 0000000000..909fa34fd3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/lock/LockType.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.lock; + +/** + * Enum used to indicate lock type + * + * @author Roy Wetherall + */ +public enum LockType {READ_ONLY_LOCK, WRITE_LOCK} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/lock/NodeLockedException.java b/source/java/org/alfresco/service/cmr/lock/NodeLockedException.java new file mode 100644 index 0000000000..7554f25218 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/lock/NodeLockedException.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.lock; + +import java.text.MessageFormat; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Node locked exception class + * + * @author Roy Wetherall + */ +public class NodeLockedException extends AlfrescoRuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3762254149525582646L; + + /** + * Error message + */ + private static final String ERROR_MESSAGE = "Can not perform operation since " + + "the node (id:{0}) is locked by another user."; + private static final String ERROR_MESSAGE_2 = "Can not perform operation {0} since " + + "the node (id:{1}) is locked by another user."; + + /** + * @param message + */ + public NodeLockedException(NodeRef nodeRef) + { + super(MessageFormat.format(ERROR_MESSAGE, new Object[]{nodeRef.getId()})); + } + + public NodeLockedException(NodeRef nodeRef, String operation) + { + super(MessageFormat.format(ERROR_MESSAGE_2, new Object[]{operation, nodeRef.getId()})); + } +} diff --git a/source/java/org/alfresco/service/cmr/lock/UnableToAquireLockException.java b/source/java/org/alfresco/service/cmr/lock/UnableToAquireLockException.java new file mode 100644 index 0000000000..b99b110938 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/lock/UnableToAquireLockException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.lock; + +import java.text.MessageFormat; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class UnableToAquireLockException extends RuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3258689892710889781L; + + /** + * Error message + */ + private final static String ERROR_MESSAGE = "The node (id: {0})could not be locked since it" + + " is already locked by antoher user."; + + /** + * Constructor + */ + public UnableToAquireLockException(NodeRef nodeRef) + { + super(MessageFormat.format(ERROR_MESSAGE, new Object[]{nodeRef.getId()})); + } +} diff --git a/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java b/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java new file mode 100644 index 0000000000..409af4c134 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.lock; + +import java.text.MessageFormat; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Runtime exception class + * + * @author Roy Wetherall + */ +public class UnableToReleaseLockException extends RuntimeException +{ + /** + * Serial verison UID + */ + private static final long serialVersionUID = 3257565088071432243L; + + /** + * Error message + */ + private static final String ERROR_MESSAGE = + "You have insufficent priveleges to realese the " + + "lock on the node (id: {0}). The node is locked by " + + "another user."; + + /** + * Constructor + */ + public UnableToReleaseLockException(NodeRef nodeRef) + { + super(MessageFormat.format(ERROR_MESSAGE, new Object[]{nodeRef.getId()})); + } +} diff --git a/source/java/org/alfresco/service/cmr/model/FileExistsException.java b/source/java/org/alfresco/service/cmr/model/FileExistsException.java new file mode 100644 index 0000000000..23310a9f8f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/model/FileExistsException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.model; + +/** + * Common, checked exception thrown when an operation fails because + * of a name clash. + * + * @author Derek Hulley + */ +public class FileExistsException extends Exception +{ + private static final long serialVersionUID = -4133713912784624118L; + + private FileInfo existing; + + public FileExistsException(FileInfo existing) + { + super("" + + (existing.isFolder() ? "Folder " : "File ") + + existing.getName() + + " already exists"); + this.existing = existing; + } + + public FileInfo getExisting() + { + return existing; + } +} diff --git a/source/java/org/alfresco/service/cmr/model/FileFolderService.java b/source/java/org/alfresco/service/cmr/model/FileFolderService.java new file mode 100644 index 0000000000..16cf4c162f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/model/FileFolderService.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.model; + +import java.util.List; + +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Provides methods specific to manipulating {@link org.alfresco.model.ContentModel#TYPE_CONTENT files} + * and {@link org.alfresco.model.ContentModel#TYPE_FOLDER folders}. + * + * @see org.alfresco.model.ContentModel + * + * @author Derek Hulley + */ +public interface FileFolderService +{ + /** + * Lists immediate child files and folders of the given context node + * + * @param contextNodeRef the node to start searching in + * @return Returns a list of matching files and folders + */ + public List list(NodeRef contextNodeRef); + + /** + * Lists all immediate child files of the given context node + * + * @param folderNodeRef the folder to start searching in + * @return Returns a list of matching files + */ + public List listFiles(NodeRef folderNodeRef); + + /** + * Lists all immediate child folders of the given context node + * + * @param contextNodeRef the node to start searching in + * @return Returns a list of matching folders + */ + public List listFolders(NodeRef contextNodeRef); + + /** + * Searches for all files and folders with the matching name pattern, + * using wildcard characters * and ?. + * + * @see #search(NodeRef, String, boolean, boolean, boolean) + */ + public List search( + NodeRef contextNodeRef, + String namePattern, + boolean includeSubFolders); + + /** + * Perform a search against the name of the files or folders within a hierarchy. + * Wildcard characters are * and ?. + * + * @param contextNodeRef the context of the search. This node will never be returned + * as part of the search results. + * @param namePattern the name of the file or folder to search for, or a + * {@link org.alfresco.util.SearchLanguageConversion#DEF_LUCENE wildcard} pattern + * to search for. + * @param fileSearch true if file types are to be included in the search results + * @param folderSearch true if folder types are to be included in the search results + * @param includeSubFolders true to search the entire hierarchy below the search context + * @return Returns a list of file or folder matches + */ + public List search( + NodeRef contextNodeRef, + String namePattern, + boolean fileSearch, + boolean folderSearch, + boolean includeSubFolders); + + /** + * Rename a file or folder in its current location + * + * @param fileFolderRef the file or folder to rename + * @param newName the new name + * @return Return the new file info + * @throws FileExistsException if a file or folder with the new name already exists + * @throws FileNotFoundException the file or folder reference doesn't exist + */ + public FileInfo rename(NodeRef fileFolderRef, String newName) throws FileExistsException, FileNotFoundException; + + /** + * Move a file or folder to a new name and/or location. + *

    + * If both the parent folder and name remain the same, then nothing is done. + * + * @param sourceNodeRef the file or folder to move + * @param targetParentRef the new parent node to move the node to - null means rename in situ + * @param newName the name to change the file or folder to - null to keep the existing name + * @return Returns the new file info + * @throws FileExistsException + * @throws FileNotFoundException + */ + public FileInfo move(NodeRef sourceNodeRef, NodeRef targetParentRef, String newName) + throws FileExistsException, FileNotFoundException; + + /** + * Copy a source file or folder. The source can be optionally renamed and optionally + * moved into another folder. + *

    + * If both the parent folder and name remain the same, then nothing is done. + * + * @param sourceNodeRef the file or folder to copy + * @param targetParentRef the new parent node to copy the node to - null means rename in situ + * @param newName the new name, or null to keep the existing name. + * @return Return the new file info + * @throws FileExistsException + * @throws FileNotFoundException + */ + public FileInfo copy(NodeRef sourceNodeRef, NodeRef targetParentRef, String newName) + throws FileExistsException, FileNotFoundException; + + /** + * Create a file or folder; or any valid node of type derived from file or folder + * + * @param parentNodeRef the parent node. The parent must be a valid + * {@link org.alfresco.model.ContentModel#TYPE_CONTAINER container}. + * @param name the name of the node + * @param typeQName the type to create + * @return Returns the new node's file information + * @throws FileExistsException + */ + public FileInfo create(NodeRef parentNodeRef, String name, QName typeQName) throws FileExistsException; + + /** + * Delete a file or folder + * + * @param nodeRef the node to delete + */ + public void delete(NodeRef nodeRef); + + /** + * Checks for the presence of, and creates as necessary, the folder structure in the provided path. + *

    + * An empty path list is not allowed as it would be impossible to necessarily return file info + * for the parent node - it might not be a folder node. + * + * @param parentNodeRef the node under which the path will be created + * @param pathElements the folder name path to create - may not be empty + * @param folderTypeQName the types of nodes to create. This must be a valid subtype of + * {@link org.alfresco.model.ContentModel#TYPE_FOLDER they folder type}. + * @return Returns the info of the last folder in the path. + */ + public FileInfo makeFolders(NodeRef parentNodeRef, List pathElements, QName folderTypeQName); + + /** + * Get the file or folder names from the root down to and including the node provided. + *

      + *
    • The root node can be of any type and is not included in the path list.
    • + *
    • Only the primary path is considered. If the target node is not a descendent of the + * root along purely primary associations, then an exception is generated.
    • + *
    • If an invalid type is encoutered along the path, then an exception is generated.
    • + *
    + * + * @param rootNodeRef the start of the returned path, or null if the store root + * node must be assumed. + * @param nodeRef a reference to the file or folder + * @return Returns a list of file/folder infos from the root (excluded) down to and + * including the destination file or folder + * @throws FileNotFoundException if the node could not be found + */ + public List getNamePath(NodeRef rootNodeRef, NodeRef nodeRef) throws FileNotFoundException; + + /** + * Resolve a file or folder name path from a given root node down to the final node. + * + * @param rootNodeRef the start of the path given, i.e. the '/' in '/A/B/C' for example + * @param pathElements a list of names in the path + * @return Returns the info of the file or folder + * @throws FileNotFoundException if no file or folder exists along the path + */ + public FileInfo resolveNamePath(NodeRef rootNodeRef, List pathElements) throws FileNotFoundException; + + /** + * Get the file info (name, folder, etc) for the given node + * + * @param nodeRef the node to get info for + * @return Returns the file info or null if the node does not represent a file or folder + */ + public FileInfo getFileInfo(NodeRef nodeRef); + + public ContentReader getReader(NodeRef nodeRef); + + public ContentWriter getWriter(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/model/FileInfo.java b/source/java/org/alfresco/service/cmr/model/FileInfo.java new file mode 100644 index 0000000000..f903b82596 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/model/FileInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.model; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Common file information. The implementations may store the properties for the lifetime + * of this instance; i.e. the values are transient and can be used as read-only values for + * a short time only. + * + * @author Derek Hulley + */ +public interface FileInfo +{ + /** + * @return Returns a reference to the low-level node representing this file + */ + public NodeRef getNodeRef(); + + /** + * @return Return true if this instance represents a folder, false if this represents a file + */ + public boolean isFolder(); + + /** + * @return Returns the name of the file or folder within the parent folder + */ + public String getName(); + + /** + * @return Returns the date the node was created + */ + public Date getCreatedDate(); + + /** + * @return Returns the modified date + */ + public Date getModifiedDate(); + + /** + * Get the content data. This is only valid for {@link #isFolder() files}. + * + * @return Returns the content data + */ + public ContentData getContentData(); + + /** + * @return Returns all the node properties + */ + public Map getProperties(); +} diff --git a/source/java/org/alfresco/service/cmr/model/FileNotFoundException.java b/source/java/org/alfresco/service/cmr/model/FileNotFoundException.java new file mode 100644 index 0000000000..61e3b8eccd --- /dev/null +++ b/source/java/org/alfresco/service/cmr/model/FileNotFoundException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.model; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Common, checked exception thrown when a file or folder could not be found + * + * @author Derek Hulley + */ +public class FileNotFoundException extends Exception +{ + private static final long serialVersionUID = 2558540174977806285L; + + public FileNotFoundException(NodeRef nodeRef) + { + super("No file or folder found for node reference: " + nodeRef); + } + + public FileNotFoundException(String msg) + { + super(msg); + } +} diff --git a/source/java/org/alfresco/service/cmr/model/package.html b/source/java/org/alfresco/service/cmr/model/package.html new file mode 100644 index 0000000000..55c2594ee8 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/model/package.html @@ -0,0 +1,11 @@ + + + + + + Model-specific services. +

    + These services give much simpler APIs for manipulating nodes structures + conforming to specific models within the data dictionary. + + diff --git a/source/java/org/alfresco/service/cmr/repository/AbstractStoreException.java b/source/java/org/alfresco/service/cmr/repository/AbstractStoreException.java new file mode 100644 index 0000000000..82cd5c8d30 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/AbstractStoreException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + + +/** + * Store-related exception that keeps a handle to the store reference + * + * @author Derek Hulley + */ +public abstract class AbstractStoreException extends RuntimeException +{ + private StoreRef storeRef; + + public AbstractStoreException(StoreRef storeRef) + { + this(null, storeRef); + } + + public AbstractStoreException(String msg, StoreRef storeRef) + { + super(msg); + this.storeRef = storeRef; + } + + /** + * @return Returns the offending store reference + */ + public StoreRef getStoreRef() + { + return storeRef; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/AspectMissingException.java b/source/java/org/alfresco/service/cmr/repository/AspectMissingException.java new file mode 100644 index 0000000000..4f480b67ff --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/AspectMissingException.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.text.MessageFormat; + +import org.alfresco.service.namespace.QName; + +/** + * Used to indicate that an aspect is missing from a node. + * + * @author Roy Wetherall + */ +public class AspectMissingException extends RuntimeException +{ + private static final long serialVersionUID = 3257852099244210228L; + + private QName missingAspect; + private NodeRef nodeRef; + + /** + * Error message + */ + private static final String ERROR_MESSAGE = "The {0} aspect is missing from this node (id: {1}). " + + "It is required for this operation."; + + /** + * Constructor + */ + public AspectMissingException(QName missingAspect, NodeRef nodeRef) + { + super(MessageFormat.format(ERROR_MESSAGE, new Object[]{missingAspect.toString(), nodeRef.getId()})); + this.missingAspect = missingAspect; + this.nodeRef = nodeRef; + } + + public QName getMissingAspect() + { + return missingAspect; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java b/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java new file mode 100644 index 0000000000..9ccd21d69f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.service.namespace.QName; + +/** + * Thrown when an operation could not be performed because a named association already + * exists between two nodes + * + * @author Derek Hulley + */ +public class AssociationExistsException extends RuntimeException +{ + private static final long serialVersionUID = 3256440317824874800L; + + private NodeRef sourceRef; + private NodeRef targetRef; + private QName qname; + + /** + * @param sourceRef the source of the association + * @param targetRef the target of the association + * @param qname the qualified name of the association + */ + public AssociationExistsException(NodeRef sourceRef, NodeRef targetRef, QName qname) + { + super(); + this.sourceRef = sourceRef; + this.targetRef = targetRef; + this.qname = qname; + } + + public NodeRef getSourceRef() + { + return sourceRef; + } + + public NodeRef getTargetRef() + { + return targetRef; + } + + public QName getQName() + { + return qname; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/AssociationRef.java b/source/java/org/alfresco/service/cmr/repository/AssociationRef.java new file mode 100644 index 0000000000..aee6645f23 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/AssociationRef.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; + +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; + +/** + * This class represents a regular, named node relationship between two nodes. + * + * @author Derek Hulley + */ +public class AssociationRef implements EntityRef, Serializable +{ + private static final long serialVersionUID = 3977867284482439475L; + + private NodeRef sourceRef; + private QName assocTypeQName; + private NodeRef targetRef; + + /** + * Construct a representation of a source --- name ----> target + * relationship. + * + * @param sourceRef + * the source reference - never null + * @param assocTypeQName + * the qualified name of the association type - never null + * @param targetRef + * the target node reference - never null. + */ + public AssociationRef(NodeRef sourceRef, QName assocTypeQName, NodeRef targetRef) + { + this.sourceRef = sourceRef; + this.assocTypeQName = assocTypeQName; + this.targetRef = targetRef; + + // check + if (sourceRef == null) + { + throw new IllegalArgumentException("Source reference may not be null"); + } + if (assocTypeQName == null) + { + throw new IllegalArgumentException("QName may not be null"); + } + if (targetRef == null) + { + throw new IllegalArgumentException("Target reference may not be null"); + } + } + + /** + * Get the qualified name of the source-target association + * + * @return Returns the qualified name of the source-target association. + */ + public QName getTypeQName() + { + return assocTypeQName; + } + + /** + * @return Returns the child node reference - never null + */ + public NodeRef getTargetRef() + { + return targetRef; + } + + /** + * @return Returns the parent node reference, which may be null if this + * represents the imaginary reference to the root node + */ + public NodeRef getSourceRef() + { + return sourceRef; + } + + /** + * Compares: + *

      + *
    • {@link #sourceRef}
    • + *
    • {@link #targetRef}
    • + *
    • {@link #assocTypeQName}
    • + *
    + */ + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof ChildAssociationRef)) + { + return false; + } + AssociationRef other = (AssociationRef) o; + + return (EqualsHelper.nullSafeEquals(this.sourceRef, other.sourceRef) + && EqualsHelper.nullSafeEquals(this.assocTypeQName, other.assocTypeQName) + && EqualsHelper.nullSafeEquals(this.targetRef, other.targetRef)); + } + + public int hashCode() + { + int hashCode = (getSourceRef() == null) ? 0 : getSourceRef().hashCode(); + hashCode = 37 * hashCode + ((getTypeQName() == null) ? 0 : getTypeQName().hashCode()); + hashCode = 37 * hashCode + getTargetRef().hashCode(); + return hashCode; + } + + public String toString() + { + StringBuffer buffer = new StringBuffer(); + buffer.append(getSourceRef()); + buffer.append(" --- ").append(getTypeQName()).append(" ---> "); + buffer.append(getTargetRef()); + return buffer.toString(); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/ChildAssociationRef.java b/source/java/org/alfresco/service/cmr/repository/ChildAssociationRef.java new file mode 100644 index 0000000000..5694920be8 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ChildAssociationRef.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; + +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; + +/** + * This class represents a child relationship between two nodes. This + * relationship is named. + *

    + * So it requires the parent node ref, the child node ref and the name of the + * child within the particular parent. + *

    + * This combination is not a unique identifier for the relationship with regard + * to structure. In use this does not matter as we have no concept of order, + * particularly in the index. + * + * @author andyh + * + */ +public class ChildAssociationRef + implements EntityRef, Comparable, Serializable +{ + private static final long serialVersionUID = 4051322336257127729L; + + private QName assocTypeQName; + private NodeRef parentRef; + private QName childQName; + private NodeRef childRef; + private boolean isPrimary; + private int nthSibling; + + + /** + * Construct a representation of a parent --- name ----> child relationship. + * + * @param assocTypeQName + * the type of the association + * @param parentRef + * the parent reference - may be null + * @param childQName + * the qualified name of the association - may be null + * @param childRef + * the child node reference. This must not be null. + * @param isPrimary + * true if this represents the primary parent-child relationship + * @param nthSibling + * the nth association with the same properties. Usually -1 to be + * ignored. + */ + public ChildAssociationRef( + QName assocTypeQName, + NodeRef parentRef, + QName childQName, + NodeRef childRef, + boolean isPrimary, + int nthSibling) + { + this.assocTypeQName = assocTypeQName; + this.parentRef = parentRef; + this.childQName = childQName; + this.childRef = childRef; + this.isPrimary = isPrimary; + this.nthSibling = nthSibling; + + // check + if (childRef == null) + { + throw new IllegalArgumentException("Child reference may not be null"); + } + } + + /** + * Constructs a non-primary, -1th sibling parent-child association + * reference. + * + * @see ChildAssociationRef#ChildAssocRef(QName, NodeRef, QName, NodeRef, boolean, int) + */ + public ChildAssociationRef(QName assocTypeQName, NodeRef parentRef, QName childQName, NodeRef childRef) + { + this(assocTypeQName, parentRef, childQName, childRef, false, -1); + } + + /** + * Compares: + *

      + *
    • {@link #assocTypeQName}
    • + *
    • {@link #parentRef}
    • + *
    • {@link #childRef}
    • + *
    • {@link #childQName}
    • + *
    + */ + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof ChildAssociationRef)) + { + return false; + } + ChildAssociationRef other = (ChildAssociationRef) o; + + return (EqualsHelper.nullSafeEquals(this.assocTypeQName, other.assocTypeQName) + && EqualsHelper.nullSafeEquals(this.parentRef, other.parentRef) + && EqualsHelper.nullSafeEquals(this.childQName, other.childQName) + && EqualsHelper.nullSafeEquals(this.childRef, other.childRef)); + } + + public int hashCode() + { + int hashCode = ((getTypeQName() == null) ? 0 : getTypeQName().hashCode()); + hashCode = 37 * hashCode + ((getParentRef() == null) ? 0 : getParentRef().hashCode()); + hashCode = 37 * hashCode + ((getQName() == null) ? 0 : getQName().hashCode()); + hashCode = 37 * hashCode + getChildRef().hashCode(); + return hashCode; + } + + /** + * @see #setNthSibling(int) + */ + public int compareTo(ChildAssociationRef another) + { + int thisVal = this.nthSibling; + int anotherVal = another.nthSibling; + return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1)); + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + sb.append("[").append(getTypeQName()).append("]"); + sb.append(getParentRef()); + sb.append(" --- ").append(getQName()).append(" ---> "); + sb.append(getChildRef()); + return sb.toString(); + } + + /** + * Get the qualified name of the association type + * + * @return Returns the qualified name of the parent-child association type + * as defined in the data dictionary. It may be null if this is the + * imaginary association to the root node. + */ + public QName getTypeQName() + { + return assocTypeQName; + } + + /** + * Get the qualified name of the parent-child association + * + * @return Returns the qualified name of the parent-child association. It + * may be null if this is the imaginary association to a root node. + */ + public QName getQName() + { + return childQName; + } + + /** + * @return Returns the child node reference - never null + */ + public NodeRef getChildRef() + { + return childRef; + } + + /** + * @return Returns the parent node reference, which may be null if this + * represents the imaginary reference to the root node + */ + public NodeRef getParentRef() + { + return parentRef; + } + + /** + * @return Returns true if this represents a primary association + */ + public boolean isPrimary() + { + return isPrimary; + } + + /** + * @return Returns the nth sibling required + */ + public int getNthSibling() + { + return nthSibling; + } + + /** + * Allows post-creation setting of the ordering index. This is a helper + * so that sorted sets and lists can be easily sorted. + *

    + * This index is in no way absolute and should change depending on + * the results that appear around this instance. Therefore, the sibling + * number cannot be used to construct, say, sibling number 5. Sibling + * number 5 will exist only in results where there are siblings 1 - 4. + * + * @param nthSibling the sibling index + */ + public void setNthSibling(int nthSibling) + { + this.nthSibling = nthSibling; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentAccessor.java b/source/java/org/alfresco/service/cmr/repository/ContentAccessor.java new file mode 100644 index 0000000000..14f88173d3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentAccessor.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.service.transaction.TransactionService; + +/** + * Interface for instances that provide read and write access to content. + * + * @author Derek Hulley + */ +public interface ContentAccessor +{ + /** + * Use this method to register any interest in events against underlying + * content streams. + * {@link #getContentOutputStream() output stream}. + *

    + * This method can only be used before the content stream has been retrieved. + *

    + * When the stream has been closed, all listeners will be called + * within a {@link #setTransactionService(TransactionService) transaction} - + * to this end, a {@link TransactionService} must have been set as well. + * + * @param listener a listener that will be called for output stream + * event notification + * + * @see #setTransactionService(TransactionService) + */ + public void addListener(ContentStreamListener listener); + + /** + * Set the transaction provider that will be used when stream listeners are called. + * + * @param transactionService a transaction provider + */ + public void setTransactionService(TransactionService transactionService); + + /** + * Gets the size of the content that this reader references. + * + * @return Returns the document byte length, or OL if the + * content doesn't {@link #exists() exist}. + */ + public long getSize(); + + /** + * Get the data representation of the content being accessed. + *

    + * The content {@link #setMimetype(String) mimetype } must be set before this + * method is called as the content data requires a mimetype whenever the + * content URL is specified. + * + * @return Returns the content data + * + * @see ContentData#ContentData(String, String, long, String) + */ + public ContentData getContentData(); + + /** + * Retrieve the URL that this accessor references + * + * @return the content URL + */ + public String getContentUrl(); + + /** + * Get the content mimetype + * + * @return Returns a content mimetype + */ + public String getMimetype(); + + /** + * Set the mimetype that must be used for accessing the content + * + * @param mimetype the content mimetype + */ + public void setMimetype(String mimetype); + + /** + * Get the encoding of the content being accessed + * + * @return Returns a valid java String encoding + */ + public String getEncoding(); + + /** + * Set the String encoding for this accessor + * + * @param encoding a java-recognised encoding format + */ + public void setEncoding(String encoding); +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentData.java b/source/java/org/alfresco/service/cmr/repository/ContentData.java new file mode 100644 index 0000000000..2e2c028fec --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentData.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.EqualsHelper; + +/** + * The compound property representing content + * + * @author Derek Hulley + */ +public class ContentData implements Serializable +{ + private static final long serialVersionUID = 8979634213050121462L; + + private static char[] INVALID_CONTENT_URL_CHARS = new char[] {'|'}; + + private final String contentUrl; + private final String mimetype; + private final long size; + private final String encoding; + + /** + * Construct a content property from a string + * + * @param contentPropertyStr the string representing the content details + * @return Returns a bean version of the string + */ + public static ContentData createContentProperty(String contentPropertyStr) + { + // get the content url + int contentUrlIndex = contentPropertyStr.indexOf("contentUrl="); + if (contentUrlIndex == -1) + { + throw new AlfrescoRuntimeException( + "ContentData string does not have a content URL: " + + contentPropertyStr); + } + int mimetypeIndex = contentPropertyStr.indexOf("|mimetype=", contentUrlIndex + 11); + if (mimetypeIndex == -1) + { + throw new AlfrescoRuntimeException( + "ContentData string does not have a mimetype: " + + contentPropertyStr); + } + int sizeIndex = contentPropertyStr.indexOf("|size=", mimetypeIndex + 10); + if (sizeIndex == -1) + { + throw new AlfrescoRuntimeException( + "ContentData string does not have a size: " + + contentPropertyStr); + } + int encodingIndex = contentPropertyStr.indexOf("|encoding=", sizeIndex + 6); + if (encodingIndex == -1) + { + throw new AlfrescoRuntimeException( + "ContentData string does not have an encoding: " + + contentPropertyStr); + } + + String contentUrl = contentPropertyStr.substring(contentUrlIndex + 11, mimetypeIndex); + if (contentUrl.length() == 0) + contentUrl = null; + String mimetype = contentPropertyStr.substring(mimetypeIndex + 10, sizeIndex); + if (mimetype.length() == 0) + mimetype = null; + String sizeStr = contentPropertyStr.substring(sizeIndex + 6, encodingIndex); + if (sizeStr.length() == 0) + sizeStr = "0"; + String encoding = contentPropertyStr.substring(encodingIndex + 10); + if (encoding.length() == 0) + encoding = null; + + long size = Long.valueOf(sizeStr); + + ContentData property = new ContentData(contentUrl, mimetype, size, encoding); + // done + return property; + } + + /** + * Constructs a new instance using the existing one as a template, but replacing the + * mimetype + * + * @param existing an existing set of content data, null to use default values + * @param mimetype the mimetype to set + * @return Returns a new, immutable instance of the data + */ + public static ContentData setMimetype(ContentData existing, String mimetype) + { + ContentData ret = new ContentData( + existing == null ? null : existing.contentUrl, + mimetype, + existing == null ? 0L : existing.size, + existing == null ? "UTF-8" : existing.encoding); + // done + return ret; + } + + /** + * Create a compound set of data representing a single instance of content. + *

    + * In order to ensure data integrity, the {@link #getMimetype() mimetype} + * must be set if the {@link #getContentUrl() content URL} is set. + * + * @param contentUrl the content URL. If this value is non-null, then the + * mimetype must be supplied. + * @param mimetype the content mimetype. This is mandatory if the contentUrl is specified. + * @param size the content size. + * @param encoding the content encoding. + */ + public ContentData(String contentUrl, String mimetype, long size, String encoding) + { + checkContentUrl(contentUrl, mimetype); + + this.contentUrl = contentUrl; + this.mimetype = mimetype; + this.size = size; + this.encoding = encoding; + } + + public boolean equals(Object obj) + { + if (obj == this) + return true; + else if (obj == null) + return false; + else if (!(obj instanceof ContentData)) + return false; + ContentData that = (ContentData) obj; + return (EqualsHelper.nullSafeEquals(this.contentUrl, that.contentUrl) && + EqualsHelper.nullSafeEquals(this.mimetype, that.mimetype) && + this.size == that.size && + EqualsHelper.nullSafeEquals(this.encoding, that.encoding)); + } + + /** + * @return Returns a string of form: contentUrl=xxx;mimetype=xxx;size=xxx;encoding=xxx + */ + public String toString() + { + StringBuilder sb = new StringBuilder(80); + sb.append("contentUrl=").append(contentUrl == null ? "" : contentUrl) + .append("|mimetype=").append(mimetype == null ? "" : mimetype) + .append("|size=").append(size) + .append("|encoding=").append(encoding == null ? "" : encoding); + return sb.toString(); + } + + /** + * @return Returns a URL identifying the specific location of the content. + * The URL must identify, within the context of the originating content + * store, the exact location of the content. + * @throws ContentIOException + */ + public String getContentUrl() + { + return contentUrl; + } + + /** + * Checks that the content URL is correct, and also that the mimetype is + * non-null if the URL is present. + * + * @param contentUrl the content URL to check + * @param mimetype + */ + private void checkContentUrl(String contentUrl, String mimetype) + { + // check the URL + if (contentUrl != null && contentUrl.length() > 0) + { + for (int i = 0; i < INVALID_CONTENT_URL_CHARS.length; i++) + { + for (int j = contentUrl.length() - 1; j > -1; j--) + { + if (contentUrl.charAt(j) == INVALID_CONTENT_URL_CHARS[i]) + { + throw new IllegalArgumentException( + "The content URL contains an invalid char: \n" + + " content URL: " + contentUrl + "\n" + + " char: " + INVALID_CONTENT_URL_CHARS[i] + "\n" + + " position: " + j); + } + } + } + // check that mimetype is present if URL is present + if (mimetype == null) + { + throw new IllegalArgumentException( + "The content mimetype must be set whenever the URL is set: \n" + + " content URL: " + contentUrl + "\n" + + " mimetype: " + mimetype); + } + } + } + + /** + * Gets content's mimetype. + * + * @return Returns a standard mimetype for the content or null if the mimetype + * is unkown + */ + public String getMimetype() + { + return mimetype; + } + + /** + * Get the content's size + * + * @return Returns the size of the content + */ + public long getSize() + { + return size; + } + + /** + * Gets the content's encoding. + * + * @return Returns a valid Java encoding, typically a character encoding, or + * null if the encoding is unkown + */ + public String getEncoding() + { + return encoding; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentDataTest.java b/source/java/org/alfresco/service/cmr/repository/ContentDataTest.java new file mode 100644 index 0000000000..6cf5eb5f65 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentDataTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.service.cmr.repository.ContentData + * + * @author Derek Hulley + */ +public class ContentDataTest extends TestCase +{ + + public ContentDataTest(String name) + { + super(name); + } + + public void testToAndFromString() throws Exception + { + ContentData property = new ContentData(null, null, 0L, null); + + // check null string + String propertyStr = property.toString(); + assertEquals("Null values not converted correctly", + "contentUrl=|mimetype=|size=0|encoding=", propertyStr); + + // convert back + ContentData checkProperty = ContentData.createContentProperty(propertyStr); + assertEquals("Conversion from string failed", property, checkProperty); + + property = new ContentData("uuu", "mmm", 123L, "eee"); + + // convert to a string + propertyStr = property.toString(); + assertEquals("Incorrect property string representation", + "contentUrl=uuu|mimetype=mmm|size=123|encoding=eee", propertyStr); + + // convert back + checkProperty = ContentData.createContentProperty(propertyStr); + assertEquals("Conversion from string failed", property, checkProperty); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentIOException.java b/source/java/org/alfresco/service/cmr/repository/ContentIOException.java new file mode 100644 index 0000000000..840d731542 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentIOException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.error.AlfrescoRuntimeException; + + +/** + * Wraps a general Exceptions that occurred while reading or writing + * content. + * + * @see Throwable#getCause() + * + * @author Derek Hulley + */ +public class ContentIOException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 3258130249983276087L; + + public ContentIOException(String msg) + { + super(msg); + } + + public ContentIOException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentReader.java b/source/java/org/alfresco/service/cmr/repository/ContentReader.java new file mode 100644 index 0000000000..f29a957de6 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentReader.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.ReadableByteChannel; + +/** + * Represents a handle to read specific content. Content may only be accessed + * once per instance. + *

    + * Implementations of this interface might be Serializable + * but client code could should check suitability before attempting to serialize + * it. + *

    + * Implementations that are able to provide inter-VM streaming, such as accessing + * WebDAV, would be Serializable. An accessor that has to access a + * local file on the server could not provide inter-VM streaming unless it specifically + * makes remote calls and opens sockets, etc. + * + * @see org.alfresco.service.cmr.repository.ContentWriter + * + * @author Derek Hulley + */ +public interface ContentReader extends ContentAccessor +{ + /** + * Convenience method to get another reader onto the underlying content. + * + * @return Returns a reader onto the underlying content + * @throws ContentIOException + */ + public ContentReader getReader() throws ContentIOException; + + /** + * Check if the {@link ContentAccessor#getContentUrl() underlying content} is present. + * + * @return Returns true if there is content at the URL refered to by this reader + */ + public boolean exists(); + + /** + * Gets the time of the last modification of the underlying content. + * + * @return Returns the last modification time using the standard long + * time, or 0L if the content doesn't {@link #exists() exist}. + * + * @see System#currentTimeMillis() + */ + public long getLastModified(); + + /** + * Convenience method to find out if this reader has been closed. + * Once closed, the content can no longer be read. This method could + * be used to wait for a particular read operation to complete, for example. + * + * @return Return true if the content input stream has been used and closed + * otherwise false. + */ + public boolean isClosed(); + + /** + * Provides low-level access to the underlying content. + *

    + * Once the stream is provided to a client it should remain active + * (subject to any timeouts) until closed by the client. + * + * @return Returns a stream that can be read at will, but must be closed when completed + * @throws ContentIOException + */ + public ReadableByteChannel getReadableChannel() throws ContentIOException; + + /** + * Get a stream to read from the underlying channel + * + * @return Returns an input stream onto the underlying channel + * @throws ContentIOException + * + * @see #getReadableChannel() + */ + public InputStream getContentInputStream() throws ContentIOException; + + /** + * Gets content from the repository. + *

    + * All resources will be closed automatically. + *

    + * Care must be taken that the bytes read from the stream are properly + * decoded according to the {@link ContentAccessor#getEncoding() encoding} + * property. + * + * @param os the stream to which to write the content + * @throws ContentIOException + * + * @see #getReadableChannel() + */ + public void getContent(OutputStream os) throws ContentIOException; + + /** + * Gets content from the repository direct to file + *

    + * All resources will be closed automatically. + * + * @param file the file to write the content to - it will be overwritten + * @throws ContentIOException + * + * @see #getContentInputStream() + */ + public void getContent(File file) throws ContentIOException; + + /** + * Gets content from the repository direct to String. + *

    + * If the {@link ContentAccessor#getEncoding() encoding } is known then it will be used + * otherwise the default system byte[] to String conversion + * will be used. + *

    + * All resources will be closed automatically. + *

    + * WARNING: This should only be used when the size of the content + * is known in advance. + * + * @return Returns a String representation of the content + * @throws ContentIOException + * + * @see #getContentString(int) + * @see #getContentInputStream() + * @see String#String(byte[]) + */ + public String getContentString() throws ContentIOException; + + /** + * Gets content from the repository direct to String, but limiting + * the string size to a given number of characters. + *

    + * If the {@link ContentAccessor#getEncoding() encoding } is known then it will be used + * otherwise the default system byte[] to String conversion + * will be used. + *

    + * All resources will be closed automatically. + * + * @param length the maximum number of characters to retrieve + * @return Returns a truncated String representation of the content + * @throws ContentIOException + * @throws java.lang.IllegalArgumentException if the length is < 0 or > {@link Integer#MAX_VALUE} + * + * @see #getContentString() + * @see #getContentInputStream() + * @see String#String(byte[]) + */ + public String getContentString(int length) throws ContentIOException; +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentService.java b/source/java/org/alfresco/service/cmr/repository/ContentService.java new file mode 100644 index 0000000000..9f702a84fe --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentService.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.namespace.QName; + +/** + * Provides methods for accessing and transforming content. + *

    + * Implementations of this service are primarily responsible for ensuring + * that the correct store is used to access content, and that reads and + * writes for the same node reference are routed to the same store instance. + *

    + * The mechanism for selecting an appropriate store is not prescribed by + * the interface, but typically the decision will be made on the grounds + * of content type. + *

    + * Whereas the content stores have no knowledge of nodes other than their + * references, the ContentService is responsible for + * ensuring that all the relevant node-content relationships are maintained. + * + * @see org.alfresco.repo.content.ContentStore + * @see org.alfresco.service.cmr.repository.ContentReader + * @see org.alfresco.service.cmr.repository.ContentWriter + * + * @author Derek Hulley + */ +public interface ContentService +{ + /** + * Gets a reader for the content associated with the given node property. + *

    + * If a content URL is present for the given node then a reader must + * be returned. The {@link ContentReader#exists() exists} method should then + * be used to detect 'missing' content. + * + * @param nodeRef a reference to a node having a content property + * @param propertyQName the name of the property, which must be of type content + * @return Returns a reader for the content associated with the node property, + * or null if no content has been written for the property + * @throws InvalidNodeRefException if the node doesn't exist + * @throws InvalidTypeException if the node is not of type content + * + * @see org.alfresco.repo.content.filestore.FileContentReader#getSafeContentReader(ContentReader, String, Object[]) + */ + public ContentReader getReader(NodeRef nodeRef, QName propertyQName) + throws InvalidNodeRefException, InvalidTypeException; + + /** + * Get a content writer for the given node property, choosing to optionally have + * the node property updated automatically when the content stream closes. + *

    + * If the update flag is off, then the state of the node property will remain unchanged + * regardless of the state of the written binary data. If the flag is on, then the node + * property will be updated on the same thread as the code that closed the write + * channel. + * + * @param nodeRef a reference to a node having a content property + * @param propertyQName the name of the property, which must be of type content + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. + * @return Returns a writer for the content associated with the node property + * @throws InvalidNodeRefException if the node doesn't exist + * @throws InvalidTypeException if the node property is not of type content + */ + public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) + throws InvalidNodeRefException, InvalidTypeException; + + /** + * Gets a writer to a temporary location. The longevity of the stored + * temporary content is determined by the system. + * + * @return Returns a writer onto a temporary location + */ + public ContentWriter getTempWriter(); + + /** + * Transforms the content from the reader and writes the content + * back out to the writer. + *

    + * The mimetypes used for the transformation must be set both on + * the {@link ContentAccessor#getMimetype() reader} and on the + * {@link ContentAccessor#getMimetype() writer}. + * + * @param reader the source content location and mimetype + * @param writer the target content location and mimetype + * @throws NoTransformerException if no transformer exists for the + * given source and target mimetypes of the reader and writer + * @throws ContentIOException if the transformation fails + */ + public void transform(ContentReader reader, ContentWriter writer) + throws NoTransformerException, ContentIOException; + + /** + * Returns whether a transformer exists that can read the content from + * the reader and write the content back out to the writer. + *

    + * The mimetypes used for the transformation must be set both on + * the {@link ContentAccessor#getMimetype() reader} and on the + * {@link ContentAccessor#getMimetype() writer}. + * + * @param reader the source content location and mimetype + * @param writer the target content location and mimetype + * + * @return true if a transformer exists, false otherwise + */ + public boolean isTransformable(ContentReader reader, ContentWriter writer); +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentStreamListener.java b/source/java/org/alfresco/service/cmr/repository/ContentStreamListener.java new file mode 100644 index 0000000000..fcb3cd1d2f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentStreamListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +/** + * Listens for notifications w.r.t. content. This includes receiving notifications + * of the opening and closing of the content streams. + * + * @author Derek Hulley + */ +public interface ContentStreamListener +{ + /** + * Called when the stream associated with a reader or writer is closed + * + * @throws ContentIOException + */ + public void contentStreamClosed() throws ContentIOException; +} diff --git a/source/java/org/alfresco/service/cmr/repository/ContentWriter.java b/source/java/org/alfresco/service/cmr/repository/ContentWriter.java new file mode 100644 index 0000000000..0662317625 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/ContentWriter.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.WritableByteChannel; + + +/** + * Represents a handle to write specific content. Content may only be accessed + * once per instance. + *

    + * Implementations of this interface might be Serializable + * but client code could should check suitability before attempting to serialize + * it. + *

    + * Implementations that are able to provide inter-VM streaming, such as accessing + * WebDAV, would be Serializable. An accessor that has to access a + * local file on the server could not provide inter-VM streaming unless it specifically + * makes remote calls and opens sockets, etc. + * + * @see org.alfresco.service.cmr.repository.ContentReader + * + * @author Derek Hulley + */ +public interface ContentWriter extends ContentAccessor +{ + /** + * Convenience method to get a reader onto newly written content. This + * method will return null if the content has not yet been written by the + * writer or if the output stream is still open. + * + * @return Returns a reader onto the underlying content that this writer + * will or has written to + * @throws ContentIOException + */ + public ContentReader getReader() throws ContentIOException; + + /** + * Convenience method to find out if this writer has been closed. + * Once closed, the content can no longer be written to and it become possible + * to get readers onto the written content. + * + * @return Return true if the content output stream has been used and closed + * otherwise false. + */ + public boolean isClosed(); + + /** + * Provides low-level access to write to repository content. + *

    + * The channel returned to the client should remain open (subject to timeouts) + * until closed by the client. All lock detection, read-only access and other + * concurrency issues are dealt with during this operation. It remains + * possible that implementations will throw exceptions when the channel is closed. + *

    + * The stream will notify any listeners according to the listener interface. + * + * @return Returns a channel with which to write content + * @throws ContentIOException + */ + public WritableByteChannel getWritableChannel() throws ContentIOException; + + /** + * Get a stream to write to the underlying channel. + * + * @return Returns an output stream onto the underlying channel + * @throws ContentIOException + * + * @see #getWritableChannel() + */ + public OutputStream getContentOutputStream() throws ContentIOException; + + /** + * Copies content from the reader. + *

    + * All resources will be closed automatically. + * + * @param reader the reader acting as the source of the content + * @throws ContentIOException + * + * @see #getWritableChannel() + */ + public void putContent(ContentReader reader) throws ContentIOException; + + /** + * Puts content to the repository + *

    + * All resources will be closed automatically. + * + * @param is the input stream from which the content will be read + * @throws ContentIOException + * + * @see #getWritableChannel() + */ + public void putContent(InputStream is) throws ContentIOException; + + /** + * Puts content to the repository direct from file + *

    + * All resources will be closed automatically. + * + * @param file the file to load the content from + * @throws ContentIOException + * + * @see #getWritableChannel() + */ + public void putContent(File file) throws ContentIOException; + + /** + * Puts content to the repository direct from String. + *

    + * If the {@link ContentAccessor#getEncoding() encoding } is known then it will be used + * otherwise the default system String to byte[] conversion + * will be used. + *

    + * All resources will be closed automatically. + * + * @param content a string representation of the content + * @throws ContentIOException + * + * @see #getWritableChannel() + * @see String#getBytes(java.lang.String) + */ + public void putContent(String content) throws ContentIOException; +} diff --git a/source/java/org/alfresco/service/cmr/repository/CopyService.java b/source/java/org/alfresco/service/cmr/repository/CopyService.java new file mode 100644 index 0000000000..86a1746e77 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/CopyService.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.service.namespace.QName; + +/** + * Node operations service interface. + *

    + * This interface provides methods to copy nodes within and across workspaces and to + * update the state of a node, with that of another node, within and across workspaces. + * + * @author Roy Wetherall + */ +public interface CopyService +{ + /** + * Creates a copy of the given node. + *

    + * If the new node resides in a different workspace the new node will + * have the same id. + *

    + * If the new node resides in the same workspace then + * the new node will have the Copy aspect applied to it which will + * reference the origional node. + *

    + * The aspects applied to source node will also be applied to destination node + * and all the property value will be duplicated accordingly. This is with the + * exception of the aspects that have been marked as having 'Non-Transferable State'. + * In this case the aspect will be applied to the copy, but the properties will take + * on the default values. + *

    + * Child associations are copied onto the destination node. If the child of + * copied association is not present in the destination workspace the child + * association is not copied. This is unless is has been specfied that the + * children of the source node should also be copied. + *

    + * Target associations are copied to the destination node. If the target of the + * association is not present in the destination workspace then the association is + * not copied. + *

    + * Source association are not copied. + * + * @param sourceNodeRef the node reference used as the source of the copy + * @param destinationParent the intended parent of the new node + * @param destinationAssocTypeQName the type of the new child assoc + * @param destinationQName the qualified name of the child association from the + * parent to the new node + * + * @return the new node reference + */ + public NodeRef copy( + NodeRef sourceNodeRef, + NodeRef destinationParent, + QName destinationAssocTypeQName, + QName destinationQName, + boolean copyChildren); + + /** + * By default children of the source node are not copied. + * + * @see NodeCopyService#copy(NodeRef, NodeRef, QName, QName, boolean) + * + * @param sourceNodeRef the node reference used as the source of the copy + * @param destinationParent the intended parent of the new node + * @param destinationAssocTypeQName the type of the new child assoc + * @param destinationQName the qualified name of the child association from the + * parent to the new node + * @return the new node reference + */ + public NodeRef copy( + NodeRef sourceNodeRef, + NodeRef destinationParent, + QName destinationAssocTypeQName, + QName destinationQName); + + /** + * Copies the state of one node on top of another. + *

    + * The state of destination node is overlayed with the state of the + * source node. Any conflicts are resolved by setting the state to + * that of the source node. + *

    + * If data (for example an association) does not exist on the source + * node, but does exist on the detination node this data is NOT deleted + * from the destination node. + *

    + * Child associations and target associations are updated on the destination + * based on the current state of the source node. + *

    + * If the node that either a child or target association points to on the source + * node is not present in the destinations workspace then the association is not + * updated to the destination node. + *

    + * All aspects found on the source node are applied to the destination node where + * missing. The properties of the apects are updated accordingly except in the case + * where the aspect has been marked as having 'Non-Transferable State'. In this case + * aspect properties will take on the values already assigned to them in the + * destination node. + * + * @param sourceNodeRef the source node reference + * @param destinationNodeRef the destination node reference + */ + public void copy(NodeRef sourceNodeRef, NodeRef destinationNodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/repository/CopyServiceException.java b/source/java/org/alfresco/service/cmr/repository/CopyServiceException.java new file mode 100644 index 0000000000..b8b2bb857e --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/CopyServiceException.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +/** + * Nodes operations service exception class. + * + * @author Roy Wetherall + */ +public class CopyServiceException extends RuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3256727273112614964L; + + /** + * Constructor + */ + public CopyServiceException() + { + super(); + } + + /** + * Constructor + * + * @param message the error message + */ + public CopyServiceException(String message) + { + super(message); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/CyclicChildRelationshipException.java b/source/java/org/alfresco/service/cmr/repository/CyclicChildRelationshipException.java new file mode 100644 index 0000000000..c64d63bb86 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/CyclicChildRelationshipException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.repo.domain.ChildAssoc; + +/** + * Thrown when a cyclic parent-child relationship is detected. + * + * @author Derek Hulley + */ +public class CyclicChildRelationshipException extends RuntimeException +{ + private static final long serialVersionUID = 3545794381924874036L; + + private ChildAssoc assoc; + + public CyclicChildRelationshipException(String msg, ChildAssoc assoc) + { + super(msg); + this.assoc = assoc; + } + + public ChildAssoc getAssoc() + { + return assoc; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/EntityRef.java b/source/java/org/alfresco/service/cmr/repository/EntityRef.java new file mode 100644 index 0000000000..02501eb797 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/EntityRef.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +/** + * A marker interface for entity reference classes. + *

    + * This is used primarily as a means of ensuring type safety in collections + * of mixed type references. + * + * @see org.alfresco.service.cmr.repository.NodeService#removeChildren(NodeRef, QName) + * + * @author Derek Hulley + */ +public interface EntityRef +{ +} diff --git a/source/java/org/alfresco/service/cmr/repository/InvalidChildAssociationRefException.java b/source/java/org/alfresco/service/cmr/repository/InvalidChildAssociationRefException.java new file mode 100644 index 0000000000..9eec28750f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/InvalidChildAssociationRefException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +/** + * Thrown when an operation cannot be performed because thechild association + * reference no longer exists. + * + * @author Derek Hulley + */ +public class InvalidChildAssociationRefException extends RuntimeException +{ + private static final long serialVersionUID = -7493054268618534572L; + + private ChildAssociationRef childAssociationRef; + + public InvalidChildAssociationRefException(ChildAssociationRef childAssociationRef) + { + this(null, childAssociationRef); + } + + public InvalidChildAssociationRefException(String msg, ChildAssociationRef childAssociationRef) + { + super(msg); + this.childAssociationRef = childAssociationRef; + } + + /** + * @return Returns the offending child association reference + */ + public ChildAssociationRef getChildAssociationRef() + { + return childAssociationRef; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/InvalidNodeRefException.java b/source/java/org/alfresco/service/cmr/repository/InvalidNodeRefException.java new file mode 100644 index 0000000000..6f3ec5dde7 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/InvalidNodeRefException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + + +/** + * Thrown when an operation cannot be performed because the node reference + * no longer exists. + * + * @author Derek Hulley + */ +public class InvalidNodeRefException extends RuntimeException +{ + private static final long serialVersionUID = 3689345520586273336L; + + private NodeRef nodeRef; + + public InvalidNodeRefException(NodeRef nodeRef) + { + this(null, nodeRef); + } + + public InvalidNodeRefException(String msg, NodeRef nodeRef) + { + super(msg); + this.nodeRef = nodeRef; + } + + /** + * @return Returns the offending node reference + */ + public NodeRef getNodeRef() + { + return nodeRef; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/InvalidStoreRefException.java b/source/java/org/alfresco/service/cmr/repository/InvalidStoreRefException.java new file mode 100644 index 0000000000..2540f04388 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/InvalidStoreRefException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + + +/** + * Thrown when an operation cannot be performed because the store reference + * no longer exists. + * + * @author Derek Hulley + */ +public class InvalidStoreRefException extends AbstractStoreException +{ + private static final long serialVersionUID = 3258126938479409463L; + + public InvalidStoreRefException(StoreRef storeRef) + { + super(storeRef); + } + + public InvalidStoreRefException(String msg, StoreRef storeRef) + { + super(msg, storeRef); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/MimetypeService.java b/source/java/org/alfresco/service/cmr/repository/MimetypeService.java new file mode 100644 index 0000000000..bb9e90b506 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/MimetypeService.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; + + +/** + * This service interface provides support for Mimetypes. + * + * @author Derek Hulley + * + */ +public interface MimetypeService +{ + /** + * Get the extension for the specified mimetype + * + * @param mimetype a valid mimetype + * @return Returns the default extension for the mimetype + * @throws AlfrescoRuntimeException if the mimetype doesn't exist + */ + public String getExtension(String mimetype); + + /** + * Get all human readable mimetype descriptions indexed by mimetype extension + * + * @return the map of displays indexed by extension + */ + public Map getDisplaysByExtension(); + + /** + * Get all human readable mimetype descriptions indexed by mimetype + * + * @return the map of displays indexed by mimetype + */ + public Map getDisplaysByMimetype(); + + /** + * Get all mimetype extensions indexed by mimetype + * + * @return the map of extension indexed by mimetype + */ + public Map getExtensionsByMimetype(); + + /** + * Get all mimetypes indexed by extension + * + * @return the map of mimetypes indexed by extension + */ + public Map getMimetypesByExtension(); + + /** + * Get all mimetypes + * + * @return all mimetypes + */ + public List getMimetypes(); + + /** + * Provides a non-null best guess of the appropriate mimetype given a + * filename. + * + * @param filename the name of the file with an optional file extension + * @return Returns the best guess mimetype or the mimetype for + * straight binary files if no extension could be found. + */ + public String guessMimetype(String filename); +} diff --git a/source/java/org/alfresco/service/cmr/repository/NoTransformerException.java b/source/java/org/alfresco/service/cmr/repository/NoTransformerException.java new file mode 100644 index 0000000000..f4871852e3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/NoTransformerException.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.text.MessageFormat; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Thrown when a transformation request cannot be honoured due to + * no transformers being present for the requested transformation. + * + * @author Derek Hulley + */ +public class NoTransformerException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 3689067335554183222L; + + private static final MessageFormat MSG = + new MessageFormat("No transformation exists between mimetypes {0} and {1}"); + + private String sourceMimetype; + private String targetMimetype; + + /** + * @param sourceMimetype the attempted source mimetype + * @param targetMimetype the attempted target mimetype + */ + public NoTransformerException(String sourceMimetype, String targetMimetype) + { + super(MSG.format(new Object[] {sourceMimetype, targetMimetype})); + this.sourceMimetype = sourceMimetype; + this.targetMimetype = targetMimetype; + } + + public String getSourceMimetype() + { + return sourceMimetype; + } + + public String getTargetMimetype() + { + return targetMimetype; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/NodeRef.java b/source/java/org/alfresco/service/cmr/repository/NodeRef.java new file mode 100644 index 0000000000..3f2eb2736f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/NodeRef.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Reference to a node + * + * @author Derek Hulley + */ +public final class NodeRef implements EntityRef, Serializable +{ + + private static final long serialVersionUID = 3760844584074227768L; + private static final String URI_FILLER = "/"; + + private final StoreRef storeRef; + private final String id; + + /** + * Construct a Node Reference from a Store Reference and Node Id + * + * @param storeRef store reference + * @param id the manually assigned identifier of the node + */ + public NodeRef(StoreRef storeRef, String id) + { + if (storeRef == null) + { + throw new IllegalArgumentException( + "Store reference may not be null"); + } + if (id == null) + { + throw new IllegalArgumentException("Node id may not be null"); + } + + this.storeRef = storeRef; + this.id = id; + } + + /** + * Construct a Node Reference from a string representation of a Node Reference. + *

    + * The string representation of a Node Reference is as follows: + *

    + *

    /
    + * + * @param nodeRef the string representation of a node ref + */ + public NodeRef(String nodeRef) + { + int lastForwardSlash = nodeRef.lastIndexOf('/'); + if(lastForwardSlash == -1) + { + throw new AlfrescoRuntimeException("Invalid node ref - does not contain forward slash: " + nodeRef); + } + this.storeRef = new StoreRef(nodeRef.substring(0, lastForwardSlash)); + this.id = nodeRef.substring(lastForwardSlash+1); + } + + public String toString() + { + return storeRef.toString() + URI_FILLER + id; + } + + /** + * Override equals for this ref type + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof NodeRef) + { + NodeRef that = (NodeRef) obj; + return (this.id.equals(that.id) + && this.storeRef.equals(that.storeRef)); + } + else + { + return false; + } + } + + /** + * Hashes on ID alone. As the number of copies of a particular node will be minimal, this is acceptable + */ + public int hashCode() + { + return id.hashCode(); + } + + /** + * @return The StoreRef part of this reference + */ + public final StoreRef getStoreRef() + { + return storeRef; + } + + /** + * @return The Node Id part of this reference + */ + public final String getId() + { + return id; + } + + /** + * Helper class to convey the status of a node. + * + * @author Derek Hulley + */ + public static class Status + { + private final String changeTxnId; + private final boolean deleted; + + public Status(String changeTxnId, boolean deleted) + { + this.changeTxnId = changeTxnId; + this.deleted = deleted; + } + /** + * @return Returns the ID of the last transaction to change the node + */ + public String getChangeTxnId() + { + return changeTxnId; + } + /** + * @return Returns true if the node has been deleted, otherwise false + */ + public boolean isDeleted() + { + return deleted; + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/repository/NodeRefTest.java b/source/java/org/alfresco/service/cmr/repository/NodeRefTest.java new file mode 100644 index 0000000000..370094105c --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/NodeRefTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.service.cmr.repository.NodeRef + * + * @author Derek Hulley + */ +public class NodeRefTest extends TestCase +{ + + public NodeRefTest(String name) + { + super(name); + } + + public void testStoreRef() throws Exception + { + StoreRef storeRef = new StoreRef("ABC", "123"); + assertEquals("toString failure", "ABC://123", storeRef.toString()); + + StoreRef storeRef2 = new StoreRef(storeRef.getProtocol(), storeRef + .getIdentifier()); + assertEquals("equals failure", storeRef, storeRef2); + } + + public void testNodeRef() throws Exception + { + StoreRef storeRef = new StoreRef("ABC", "123"); + NodeRef nodeRef = new NodeRef(storeRef, "456"); + assertEquals("toString failure", "ABC://123/456", nodeRef.toString()); + + NodeRef nodeRef2 = new NodeRef(storeRef, "456"); + assertEquals("equals failure", nodeRef, nodeRef2); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/NodeService.java b/source/java/org/alfresco/service/cmr/repository/NodeService.java new file mode 100644 index 0000000000..94a4c57325 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/NodeService.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.InvalidAspectException; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; + +/** + * Interface for public and internal node and store operations. + * + * @author Derek Hulley + */ +public interface NodeService +{ + /** + * Gets a list of all available node store references + * + * @return Returns a list of store references + */ + public List getStores(); + + /** + * Create a new store for the given protocol and identifier. The implementation + * may create the store in any number of locations, including a database or + * Subversion. + * + * @param protocol the implementation protocol + * @param identifier the protocol-specific identifier + * @return Returns a reference to the store + * @throws StoreExistsException + */ + public StoreRef createStore(String protocol, String identifier) throws StoreExistsException; + + /** + * @param storeRef a reference to the store to look for + * @return Returns true if the store exists, otherwise false + */ + public boolean exists(StoreRef storeRef); + + /** + * @param nodeRef a reference to the node to look for + * @return Returns true if the node exists, otherwise false + */ + public boolean exists(NodeRef nodeRef); + + /** + * Gets the ID of the last transaction that caused the node to change. This includes + * deletions, so it is possible that the node being referenced no longer exists. + * If the node never existed, then null is returned. + * + * @param nodeRef a reference to a current or previously existing node + * @return Returns the status of the node, or null if the node never existed + */ + public NodeRef.Status getNodeStatus(NodeRef nodeRef); + + /** + * @param storeRef a reference to an existing store + * @return Returns a reference to the root node of the store + * @throws InvalidStoreRefException if the store could not be found + */ + public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException; + + /** + * @see #createNode(NodeRef, QName, QName, QName, Map) + */ + public ChildAssociationRef createNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName) + throws InvalidNodeRefException, InvalidTypeException; + + /** + * Creates a new, non-abstract, real node as a primary child of the given parent node. + * + * @param parentRef the parent node + * @param assocTypeQName the type of the association to create. This is used + * for verification against the data dictionary. + * @param assocQName the qualified name of the association + * @param nodeTypeQName a reference to the node type + * @param properties optional map of properties to keyed by their qualified names + * @return Returns a reference to the newly created child association + * @throws InvalidNodeRefException if the parent reference is invalid + * @throws InvalidTypeException if the node type reference is not recognised + * + * @see org.alfresco.service.cmr.dictionary.DictionaryService + */ + public ChildAssociationRef createNode( + NodeRef parentRef, + QName assocTypeQName, + QName assocQName, + QName nodeTypeQName, + Map properties) + throws InvalidNodeRefException, InvalidTypeException; + + /** + * Moves the primary location of the given node. + *

    + * This involves changing the node's primary parent and possibly the name of the + * association referencing it. + * + * @param nodeToMoveRef the node to move + * @param newParentRef the new parent of the moved node + * @param assocTypeQName the type of the association to create. This is used + * for verification against the data dictionary. + * @param assocQName the qualified name of the new child association + * @return Returns a reference to the newly created child association + * @throws InvalidNodeRefException if either the parent node or move node reference is invalid + * @throws CyclicChildRelationshipException if the child partakes in a cyclic relationship after the add + * + * @see #getPrimaryParent(NodeRef) + */ + public ChildAssociationRef moveNode( + NodeRef nodeToMoveRef, + NodeRef newParentRef, + QName assocTypeQName, + QName assocQName) + throws InvalidNodeRefException; + + /** + * Set the ordering index of the child association. This affects the ordering of + * of the return values of methods that return a set of children or child + * associations. + * + * @param childAssocRef the child association that must be moved in the order + * @param index an arbibrary index that will affect the return order + * + * @see #getChildAssocs(NodeRef) + * @see #getChildAssocs(NodeRef, QNamePattern, QNamePattern) + * @see ChildAssociationRef#getNthSibling() + */ + public void setChildAssociationIndex( + ChildAssociationRef childAssocRef, + int index) + throws InvalidChildAssociationRefException; + + /** + * @param nodeRef + * @return Returns the type name + * @throws InvalidNodeRefException if the node could not be found + * + * @see org.alfresco.service.cmr.dictionary.DictionaryService + */ + public QName getType(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * Re-sets the type of the node. Can be called in order specialise a node to a sub-type. + * + * This should be used with caution since calling it changes the type of the node and thus + * implies a different set of aspects, properties and associations. It is the calling codes + * responsibility to ensure that the node is in a approriate state after changing the type. + * + * @param nodeRef the node reference + * @param typeQName the type QName + * + * @since 1.1 + */ + public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException; + + /** + * Applies an aspect to the given node. After this method has been called, + * the node with have all the aspect-related properties present + * + * @param nodeRef + * @param aspectTypeQName the aspect to apply to the node + * @param aspectProperties a minimum of the mandatory properties required for + * the aspect + * @throws InvalidNodeRefException + * @throws InvalidAspectException if the class reference is not to a valid aspect + * + * @see org.alfresco.service.cmr.dictionary.DictionaryService#getAspect(QName) + * @see org.alfresco.service.cmr.dictionary.ClassDefinition#getProperties() + */ + public void addAspect( + NodeRef nodeRef, + QName aspectTypeQName, + Map aspectProperties) + throws InvalidNodeRefException, InvalidAspectException; + + /** + * Remove an aspect and all related properties from a node + * + * @param nodeRef + * @param aspectTypeQName the type of aspect to remove + * @throws InvalidNodeRefException if the node could not be found + * @throws InvalidAspectException if the the aspect is unknown or if the + * aspect is mandatory for the class of the node + */ + public void removeAspect(NodeRef nodeRef, QName aspectTypeQName) + throws InvalidNodeRefException, InvalidAspectException; + + /** + * Determines if a given aspect is present on a node. Aspects may only be + * removed if they are NOT mandatory. + * + * @param nodeRef + * @param aspectRef + * @return Returns true if the aspect has been applied to the given node, + * otherwise false + * @throws InvalidNodeRefException if the node could not be found + * @throws InvalidAspectException if the aspect reference is invalid + */ + public boolean hasAspect(NodeRef nodeRef, QName aspectRef) + throws InvalidNodeRefException, InvalidAspectException; + + /** + * @param nodeRef + * @return Returns a set of all aspects applied to the node, including mandatory + * aspects + * @throws InvalidNodeRefException if the node could not be found + */ + public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * Deletes the given node. + *

    + * All associations (both children and regular node associations) + * will be deleted, and where the given node is the primary parent, + * the children will also be cascade deleted. + * + * @param nodeRef reference to a node within a store + * @throws InvalidNodeRefException if the reference given is invalid + */ + public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * Makes a parent-child association between the given nodes. Both nodes must belong to the same store. + *

    + * + * + * @param parentRef + * @param childRef + * @param assocTypeQName the qualified name of the association type as defined in the datadictionary + * @param qname the qualified name of the association + * @return Returns a reference to the newly created child association + * @throws InvalidNodeRefException if the parent or child nodes could not be found + * @throws CyclicChildRelationshipException if the child partakes in a cyclic relationship after the add + */ + public ChildAssociationRef addChild( + NodeRef parentRef, + NodeRef childRef, + QName assocTypeQName, + QName qname) throws InvalidNodeRefException; + + /** + * Severs all parent-child relationships between two nodes. + *

    + * The child node will be cascade deleted if one of the associations was the + * primary association, i.e. the one with which the child node was created. + * + * @param parentRef the parent end of the association + * @param childRef the child end of the association + * @return Returns a collection of deleted entities - both associations and node references. + * @throws InvalidNodeRefException if the parent or child nodes could not be found + */ + public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException; + + /** + * @param nodeRef + * @return Returns all properties keyed by their qualified name + * @throws InvalidNodeRefException if the node could not be found + */ + public Map getProperties(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * @param nodeRef + * @param qname the qualified name of the property + * @return Returns the value of the property, or null if not yet set + * @throws InvalidNodeRefException if the node could not be found + */ + public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException; + + /** + * Set the values of all properties to be an Serializable instances. + * The properties given must still fulfill the requirements of the class and + * aspects relevant to the node. + *

    + * NOTE: Null values are allowed. + * + * @param nodeRef + * @param properties all the properties of the node keyed by their qualified names + * @throws InvalidNodeRefException if the node could not be found + */ + public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException; + + /** + * Sets the value of a property to be any Serializable instance. + * To remove a property value, use {@link #getProperties(NodeRef)}, remove the + * value and call {@link #setProperties(NodeRef, Map)}. + *

    + * NOTE: Null values are allowed. + * + * @param nodeRef + * @param qname the fully qualified name of the property + * @param propertyValue the value of the property - never null + * @throws InvalidNodeRefException if the node could not be found + */ + public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException; + + /** + * @param nodeRef the child node + * @return Returns a list of all parent-child associations that exist where the given + * node is the child + * @throws InvalidNodeRefException if the node could not be found + * + * @see #getParentAssocs(NodeRef, QNamePattern, QNamePattern) + */ + public List getParentAssocs(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * Gets all parent associations where the pattern of the association qualified + * name is a match + *

    + * The resultant list is ordered by (a) explicit index and (b) association creation time. + * + * @param nodeRef the child node + * @param typeQNamePattern the pattern that the type qualified name of the association must match + * @param qnamePattern the pattern that the qnames of the assocs must match + * @return Returns a list of all parent-child associations that exist where the given + * node is the child + * @throws InvalidNodeRefException if the node could not be found + * + * @see ChildAssociationRef#getNthSibling() + * @see #setChildAssociationIndex(ChildAssociationRef, int) + * @see QName + * @see org.alfresco.service.namespace.RegexQNamePattern#MATCH_ALL + */ + public List getParentAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern, QNamePattern qnamePattern) + throws InvalidNodeRefException; + + /** + * Get all child associations of the given node. + *

    + * The resultant list is ordered by (a) explicit index and (b) association creation time. + * + * @param nodeRef the parent node - usually a container + * @return Returns a collection of ChildAssocRef instances. If the + * node is not a container then the result will be empty. + * @throws InvalidNodeRefException if the node could not be found + * + * @see #getChildAssocs(NodeRef, QNamePattern, QNamePattern) + * @see #setChildAssociationIndex(ChildAssociationRef, int) + * @see ChildAssociationRef#getNthSibling() + */ + public List getChildAssocs(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * Gets all child associations where the pattern of the association qualified + * name is a match. + * + * @param nodeRef the parent node - usually a container + * @param typeQNamePattern the pattern that the type qualified name of the association must match + * @param qnamePattern the pattern that the qnames of the assocs must match + * @return Returns a list of ChildAssocRef instances. If the + * node is not a container then the result will be empty. + * @throws InvalidNodeRefException if the node could not be found + * + * @see QName + * @see org.alfresco.service.namespace.RegexQNamePattern#MATCH_ALL + */ + public List getChildAssocs( + NodeRef nodeRef, + QNamePattern typeQNamePattern, + QNamePattern qnamePattern) + throws InvalidNodeRefException; + + /** + * Fetches the primary parent-child relationship. + *

    + * For a root node, the parent node reference will be null. + * + * @param nodeRef + * @return Returns the primary parent-child association of the node + * @throws InvalidNodeRefException if the node could not be found + */ + public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * + * @param sourceRef a reference to a real node + * @param targetRef a reference to a node + * @param assocTypeQName the qualified name of the association type + * @return Returns a reference to the new association + * @throws InvalidNodeRefException if either of the nodes could not be found + * @throws AssociationExistsException + */ + public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) + throws InvalidNodeRefException, AssociationExistsException; + + /** + * + * @param sourceRef the associaton source node + * @param targetRef the association target node + * @param assocTypeQName the qualified name of the association type + * @throws InvalidNodeRefException if either of the nodes could not be found + */ + public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) + throws InvalidNodeRefException; + + /** + * Fetches all associations from the given source where the associations' + * qualified names match the pattern provided. + * + * @param sourceRef the association source + * @param qnamePattern the association qname pattern to match against + * @return Returns a list of NodeAssocRef instances for which the + * given node is a source + * @throws InvalidNodeRefException if the source node could not be found + * + * @see QName + * @see org.alfresco.service.namespace.RegexQNamePattern#MATCH_ALL + */ + public List getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern) + throws InvalidNodeRefException; + + /** + * Fetches all associations to the given target where the associations' + * qualified names match the pattern provided. + * + * @param targetRef the association target + * @param qnamePattern the association qname pattern to match against + * @return Returns a list of NodeAssocRef instances for which the + * given node is a target + * @throws InvalidNodeRefException + * + * @see QName + * @see org.alfresco.service.namespace.RegexQNamePattern#MATCH_ALL + */ + public List getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern) + throws InvalidNodeRefException; + + /** + * The root node has an entry in the path(s) returned. For this reason, there + * will always be at least one path element in the returned path(s). + * The first element will have a null parent reference and qname. + * + * @param nodeRef + * @return Returns the path to the node along the primary node path + * @throws InvalidNodeRefException if the node could not be found + * + * @see #getPaths(NodeRef, boolean) + */ + public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException; + + /** + * The root node has an entry in the path(s) returned. For this reason, there + * will always be at least one path element in the returned path(s). + * The first element will have a null parent reference and qname. + * + * @param nodeRef + * @param primaryOnly true if only the primary path must be retrieved. If true, the + * result will have exactly one entry. + * @return Returns a List of all possible paths to the given node + * @throws InvalidNodeRefException if the node could not be found + */ + public List getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException; +} diff --git a/source/java/org/alfresco/service/cmr/repository/Path.java b/source/java/org/alfresco/service/cmr/repository/Path.java new file mode 100644 index 0000000000..dd03bf18f7 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/Path.java @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.LinkedList; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.search.ISO9075; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + +/** + * Representation of a simple path e.g. + *

    + *   /x/y/z
    + * 
    + * In the above example, there will be 4 elements, the first being a reference + * to the root node, followed by qname elements for x, x and z. + *

    + * Methods and constructors are available to construct a Path instance + * from a path string or by building the path incrementally, including the ability to + * append and prepend path elements. + *

    + * Path elements supported: + *

      + *
    • /{namespace}name fully qualified element
    • + *
    • /name element using default namespace
    • + *
    • /{namespace}name[n] nth sibling
    • + *
    • /name[n] nth sibling using default namespace
    • + *
    • /descendant-or-self::node() descendent or self
    • + *
    • /. self
    • + *
    • /.. parent
    • + *
    + * + * @author Derek Hulley + */ +public final class Path implements Iterable, Serializable +{ + private static final long serialVersionUID = 3905520514524328247L; + private LinkedList elements; + + public Path() + { + // use linked list so as random access is not required, but both prepending and appending is + elements = new LinkedList(); + } + + /** + * @return Returns a typed iterator over the path elements + */ + public Iterator iterator() + { + return elements.iterator(); + } + + /** + * Add a path element to the beginning of the path. This operation is useful in cases where + * a path is built by traversing up a hierarchy. + * + * @param pathElement + * @return Returns this instance of the path + */ + public Path prepend(Path.Element pathElement) + { + elements.addFirst(pathElement); + return this; + } + + /** + * Merge the given path into the beginning of this path. + * + * @param path + * @return Returns this instance of the path + */ + public Path prepend(Path path) + { + elements.addAll(0, path.elements); + return this; + } + + /** + * Appends a path element to the end of the path + * + * @param pathElement + * @return Returns this instance of the path + */ + public Path append(Path.Element pathElement) + { + elements.addLast(pathElement); + return this; + } + + /** + * Append the given path of this path. + * + * @param path + * @return Returns this instance of the path + */ + public Path append(Path path) + { + elements.addAll(path.elements); + return this; + } + + /** + * @return Returns the first element in the path or null if the path is empty + */ + public Element first() + { + return elements.getFirst(); + } + + /** + * @return Returns the last element in the path or null if the path is empty + */ + public Element last() + { + return elements.getLast(); + } + + public int size() + { + return elements.size(); + } + + public Element get(int n) + { + return elements.get(n); + } + + /** + * @return Returns a string path made up of the component elements of this instance + */ + public String toString() + { + StringBuilder sb = new StringBuilder(128); + for (Element element : elements) + { + if((sb.length() > 1) || ((sb.length() == 1) && (sb.charAt(0) != '/'))) + { + sb.append("/"); + } + sb.append(element.getElementString()); + } + return sb.toString(); + } + + /** + * @return Returns a string path made up of the component elements of this instance (prefixed where appropriate) + */ + public String toPrefixString(NamespacePrefixResolver resolver) + { + StringBuilder sb = new StringBuilder(128); + for (Element element : elements) + { + if((sb.length() > 1) || ((sb.length() == 1) && (sb.charAt(0) != '/'))) + { + sb.append("/"); + } + sb.append(element.getPrefixedString(resolver)); + } + return sb.toString(); + } + + /** + * Return the human readable form of the specified node Path. Slow version of the method + * that extracts the name of each node in the Path from the supplied NodeService. + * + * @return human readable form of the Path excluding the final element + */ + public String toDisplayPath(NodeService nodeService) + { + StringBuilder buf = new StringBuilder(64); + + for (int i=0; i (elements.size() -1)) + { + throw new IndexOutOfBoundsException("Start index " + start + " must be between 0 and " + (elements.size() -1)); + } + if (end < 0 || end > (elements.size() -1)) + { + throw new IndexOutOfBoundsException("End index " + end + " must be between 0 and " + (elements.size() -1)); + } + if (end < start) + { + throw new IndexOutOfBoundsException("End index " + end + " cannot be before start index " + start); + } + Path subPath = new Path(); + for (int i = start; i <= end; i++) + { + subPath.append(this.get(i)); + } + return subPath; + } + + /** + * Override equals to check equality of Path instances + */ + public boolean equals(Object o) + { + if(o == this) + { + return true; + } + if(!(o instanceof Path)) + { + return false; + } + Path other = (Path)o; + return this.elements.equals(other.elements); + } + + /** + * Override hashCode to check hash equality of Path instances + */ + public int hashCode() + { + return elements.hashCode(); + } + + /** + * Represents a path element. + *

    + * In /x/y/z, elements are x, y and z. + */ + public abstract static class Element implements Serializable + { + /** + * @return Returns the path element portion including leading '/' and never null + */ + public abstract String getElementString(); + + /** + * @param resolver namespace prefix resolver + * @return the path element portion (with namespaces converted to prefixes) + */ + public String getPrefixedString(NamespacePrefixResolver resolver) + { + return getElementString(); + } + + /** + * @see #getElementString() + */ + public String toString() + { + return getElementString(); + } + } + + /** + * Represents a qualified path between a parent and a child node, + * including the sibling to retrieve e.g. /{namespace}name[5] + */ + public static class ChildAssocElement extends Element + { + private static final long serialVersionUID = 3689352104636790840L; + + private ChildAssociationRef ref; + + /** + * @param ref a reference to the specific parent-child association + */ + public ChildAssocElement(ChildAssociationRef ref) + { + this.ref = ref; + } + + @Override + public String getElementString() + { + return createElementString(null); + } + + @Override + public String getPrefixedString(NamespacePrefixResolver resolver) + { + return createElementString(resolver); + } + + public ChildAssociationRef getRef() + { + return ref; + } + + @Override + public boolean equals(Object o) + { + if(o == this) + { + return true; + } + if(!(o instanceof ChildAssocElement)) + { + return false; + } + ChildAssocElement other = (ChildAssocElement)o; + return this.ref.equals(other.ref); + } + + @Override + public int hashCode() + { + return ref.hashCode(); + } + + private String createElementString(NamespacePrefixResolver resolver) + { + StringBuilder sb = new StringBuilder(32); + if (ref.getParentRef() == null) + { + sb.append("/"); + } + else + { + // a parent is present + sb.append(resolver == null ? ISO9075.getXPathName(ref.getQName()) : ISO9075.getXPathName(ref.getQName(), resolver)); + } + if (ref.getNthSibling() > -1) + { + sb.append("[").append(ref.getNthSibling()).append("]"); + } + return sb.toString(); + } + } + + /** + * Represents a qualified path to an attribute, + * including the sibling for repeated properties/attributes to retrieve e.g. /@{namespace}name[5] + */ + public static class AttributeElement extends Element + { + private static final long serialVersionUID = 3256727281668863544L; + + private QName attribute; + private int position = -1; + + /** + * @param ref a reference to the specific parent-child association + */ + public AttributeElement(QName attribute) + { + this.attribute = attribute; + } + + public AttributeElement(QName attribute, int position) + { + this(attribute); + this.position = position; + } + + @Override + public String getElementString() + { + return createElementString(null); + } + + @Override + public String getPrefixedString(NamespacePrefixResolver resolver) + { + return createElementString(resolver); + } + + private String createElementString(NamespacePrefixResolver resolver) + { + StringBuilder sb = new StringBuilder(32); + sb.append("@").append(resolver == null ? ISO9075.getXPathName(attribute) : ISO9075.getXPathName(attribute, resolver)); + + if (position > -1) + { + sb.append("[").append(position).append("]"); + } + return sb.toString(); + } + + public QName getQName() + { + return attribute; + } + + public int position() + { + return position; + } + + public boolean equals(Object o) + { + if(o == this) + { + return true; + } + if(!(o instanceof AttributeElement)) + { + return false; + } + AttributeElement other = (AttributeElement)o; + return this.getQName().equals(other.getQName()) && (this.position() == other.position()); + } + + public int hashCode() + { + return getQName().hashCode()*32 + position(); + } + + } + + /** + * Represents the // or /descendant-or-self::node() xpath element + */ + public static class DescendentOrSelfElement extends Element + { + private static final long serialVersionUID = 3258410616875005237L; + + public String getElementString() + { + return "descendant-or-self::node()"; + } + + public boolean equals(Object o) + { + if(o == this) + { + return true; + } + if(!(o instanceof DescendentOrSelfElement)) + { + return false; + } + return true; + } + + public int hashCode() + { + return "descendant-or-self::node()".hashCode(); + } + + } + + /** + * Represents the /. xpath element + */ + public static class SelfElement extends Element + { + private static final long serialVersionUID = 3834311739151300406L; + + public String getElementString() + { + return "."; + } + + public boolean equals(Object o) + { + if(o == this) + { + return true; + } + if(!(o instanceof SelfElement)) + { + return false; + } + return true; + } + + public int hashCode() + { + return ".".hashCode(); + } + } + + /** + * Represents the /.. xpath element + */ + public static class ParentElement extends Element + { + private static final long serialVersionUID = 3689915080477456179L; + + public String getElementString() + { + return ".."; + } + + public boolean equals(Object o) + { + if(o == this) + { + return true; + } + if(!(o instanceof ParentElement)) + { + return false; + } + return true; + } + + public int hashCode() + { + return "..".hashCode(); + } + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/PathTest.java b/source/java/org/alfresco/service/cmr/repository/PathTest.java new file mode 100644 index 0000000000..cc2f7b6537 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/PathTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.service.namespace.QName; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.service.cmr.repository.Path + * + * @author Derek Hulley + */ +public class PathTest extends TestCase +{ + private Path absolutePath; + private Path relativePath; + private QName typeQName; + private QName qname; + private StoreRef storeRef; + private NodeRef parentRef; + private NodeRef childRef; + + public PathTest(String name) + { + super(name); + } + + public void setUp() throws Exception + { + super.setUp(); + absolutePath = new Path(); + relativePath = new Path(); + typeQName = QName.createQName("http://www.alfresco.org/PathTest/1.0", "testType"); + qname = QName.createQName("http://www.google.com", "documentx"); + storeRef = new StoreRef("x", "y"); + parentRef = new NodeRef(storeRef, "P"); + childRef = new NodeRef(storeRef, "C"); + } + + public void testQNameElement() throws Exception + { + // plain + Path.Element element = new Path.ChildAssocElement(new ChildAssociationRef(typeQName, parentRef, qname, childRef)); + assertEquals("Element string incorrect", + qname.toString(), + element.getElementString()); + // sibling + element = new Path.ChildAssocElement(new ChildAssociationRef(typeQName, parentRef, qname, childRef, true, 5)); + assertEquals("Element string incorrect", "{http://www.google.com}documentx[5]", element.getElementString()); + } + + public void testElementTypes() throws Exception + { + Path.Element element = new Path.DescendentOrSelfElement(); + assertEquals("DescendentOrSelf element incorrect", + "descendant-or-self::node()", + element.getElementString()); + + element = new Path.ParentElement(); + assertEquals("Parent element incorrect", "..", element.getElementString()); + + element = new Path.SelfElement(); + assertEquals("Self element incorrect", ".", element.getElementString()); + } + + public void testAppendingAndPrepending() throws Exception + { + Path.Element element0 = new Path.ChildAssocElement(new ChildAssociationRef(null, null, null, parentRef)); + Path.Element element1 = new Path.ChildAssocElement(new ChildAssociationRef(typeQName, parentRef, qname, childRef, true, 4)); + Path.Element element2 = new Path.DescendentOrSelfElement(); + Path.Element element3 = new Path.ParentElement(); + Path.Element element4 = new Path.SelfElement(); + // append them all to the path + absolutePath.append(element0).append(element1).append(element2).append(element3).append(element4); + relativePath.append(element1).append(element2).append(element3).append(element4); + // check + assertEquals("Path appending didn't work", + "/{http://www.google.com}documentx[4]/descendant-or-self::node()/../.", + absolutePath.toString()); + + // copy the path + Path copy = new Path(); + copy.append(relativePath).append(relativePath); + // check + assertEquals("Path appending didn't work", + relativePath.toString() + "/" + relativePath.toString(), + copy.toString()); + + // prepend + relativePath.prepend(element2); + // check + assertEquals("Prepending didn't work", + "descendant-or-self::node()/{http://www.google.com}documentx[4]/descendant-or-self::node()/../.", + relativePath.toString()); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/repository/StoreExistsException.java b/source/java/org/alfresco/service/cmr/repository/StoreExistsException.java new file mode 100644 index 0000000000..1273cb1177 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/StoreExistsException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + + +/** + * Thrown when an operation cannot be performed because the store reference + * no longer exists. + * + * @author Derek Hulley + */ +public class StoreExistsException extends AbstractStoreException +{ + private static final long serialVersionUID = 3906369320370975030L; + + public StoreExistsException(StoreRef storeRef) + { + super(storeRef); + } + + public StoreExistsException(String msg, StoreRef storeRef) + { + super(msg, storeRef); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/StoreRef.java b/source/java/org/alfresco/service/cmr/repository/StoreRef.java new file mode 100644 index 0000000000..b9b2faba13 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/StoreRef.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Reference to a node store + * + * @author Derek Hulley + */ +public final class StoreRef implements EntityRef, Serializable +{ + private static final long serialVersionUID = 3905808565129394486L; + + public static final String PROTOCOL_WORKSPACE = "workspace"; + + public static final String URI_FILLER = "://"; + + private final String protocol; + private final String identifier; + + /** + * @param protocol + * well-known protocol for the store, e.g. workspace or + * versionstore + * @param identifier + * the identifier, which may be specific to the protocol + */ + public StoreRef(String protocol, String identifier) + { + if (protocol == null) + { + throw new IllegalArgumentException("Store protocol may not be null"); + } + if (identifier == null) + { + throw new IllegalArgumentException("Store identifier may not be null"); + } + + this.protocol = protocol; + this.identifier = identifier; + } + + public StoreRef(String string) + { + int dividerPatternPosition = string.indexOf(URI_FILLER); + if(dividerPatternPosition == -1) + { + throw new AlfrescoRuntimeException("Invalid store ref: Does not contain " + URI_FILLER + " " + string); + } + this.protocol = string.substring(0, dividerPatternPosition); + this.identifier = string.substring(dividerPatternPosition+3); + } + + public String toString() + { + return protocol + URI_FILLER + identifier; + } + + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof StoreRef) + { + StoreRef that = (StoreRef) obj; + return (this.protocol.equals(that.protocol) + && this.identifier.equals(that.identifier)); + } else + { + return false; + } + } + + /** + * Creates a hashcode from both the {@link #getProtocol()} and {@link #getIdentifier()} + */ + public int hashCode() + { + return (protocol.hashCode() + identifier.hashCode()); + } + + public String getProtocol() + { + return protocol; + } + + public String getIdentifier() + { + return identifier; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateException.java b/source/java/org/alfresco/service/cmr/repository/TemplateException.java new file mode 100644 index 0000000000..c4e47a0f7c --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/TemplateException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * @author Kevin Roast + */ +public class TemplateException extends AlfrescoRuntimeException +{ + /** + * @param msgId + */ + public TemplateException(String msgId) + { + super(msgId); + } + + /** + * @param msgId + * @param cause + */ + public TemplateException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + /** + * @param msgId + * @param params + */ + public TemplateException(String msgId, Object[] params) + { + super(msgId, params); + } + + /** + * @param msgId + * @param msgParams + * @param cause + */ + public TemplateException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateImageResolver.java b/source/java/org/alfresco/service/cmr/repository/TemplateImageResolver.java new file mode 100644 index 0000000000..7cb0878552 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/TemplateImageResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +/** + * Interface contract for the conversion of file name to a fully qualified icon image path for use by + * the templating engine. + * + * Generally this contract will be implemented by classes that have access to say the webserver + * context which can be used to generate an icon image for a specific filename. + * + * @author Kevin Roast + */ +public interface TemplateImageResolver +{ + /** + * Resolve the qualified icon image path for the specified filename + * + * @param filename The file name to resolve image path for + * @param small True to resolve to the small 16x16 image, else large 32x32 image + */ + public String resolveImagePathForName(String filename, boolean small); +} diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateNode.java b/source/java/org/alfresco/service/cmr/repository/TemplateNode.java new file mode 100644 index 0000000000..9c26d3032e --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/TemplateNode.java @@ -0,0 +1,614 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Serializable; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.template.NamePathResultsMap; +import org.alfresco.repo.template.XPathResultsMap; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNameMap; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.InputSource; + +import freemarker.ext.dom.NodeModel; + +/** + * Node class specific for use by Template pages that support Bean objects as part of the model. + * The default template engine FreeMarker can use these objects and they are provided to support it. + * A single method is completely freemarker specific - getXmlNodeModel() + *

    + * The class exposes Node properties, children as dynamically populated maps and lists. + *

    + * Various helper methods are provided to access common and useful node variables such + * as the content url and type information. + * + * @author Kevin Roast + */ +public final class TemplateNode implements Serializable +{ + private static final long serialVersionUID = 1234390333739034171L; + + private static Log logger = LogFactory.getLog(TemplateNode.class); + + private final static String NAMESPACE_BEGIN = "" + QName.NAMESPACE_BEGIN; + private final static String CONTENT_DEFAULT_URL = "/download/direct/{0}/{1}/{2}/{3}"; + private final static String CONTENT_PROP_URL = "/download/direct/{0}/{1}/{2}/{3}?property={4}"; + + /** The children of this node */ + private List children = null; + + /** The associations from this node */ + private Map> assocs = null; + + /** Cached values */ + private NodeRef nodeRef; + private String name; + private QName type; + private String path; + private String id; + private Set aspects = null; + private QNameMap properties; + private boolean propsRetrieved = false; + private ServiceRegistry services = null; + private Boolean isDocument = null; + private Boolean isContainer = null; + private String displayPath = null; + private String mimetype = null; + private Long size = null; + private TemplateImageResolver imageResolver = null; + private TemplateNode parent = null; + + + /** + * Constructor + * + * @param nodeRef The NodeRef this Node wrapper represents + * @param services The ServiceRegistry the TemplateNode can use to access services + * @param resolver Image resolver to use to retrieve icons + */ + public TemplateNode(NodeRef nodeRef, ServiceRegistry services, TemplateImageResolver resolver) + { + if (nodeRef == null) + { + throw new IllegalArgumentException("NodeRef must be supplied."); + } + + if (services == null) + { + throw new IllegalArgumentException("The ServiceRegistry must be supplied."); + } + + this.nodeRef = nodeRef; + this.id = nodeRef.getId(); + this.services = services; + this.imageResolver = resolver; + + this.properties = new QNameMap(this.services.getNamespaceService()); + } + + /** + * @return The GUID for the node + */ + public String getId() + { + return this.id; + } + + /** + * @return Returns the NodeRef this Node object represents + */ + public NodeRef getNodeRef() + { + return this.nodeRef; + } + + /** + * @return Returns the type. + */ + public QName getType() + { + if (this.type == null) + { + this.type = this.services.getNodeService().getType(this.nodeRef); + } + + return type; + } + + /** + * @return The display name for the node + */ + public String getName() + { + if (this.name == null) + { + // try and get the name from the properties first + this.name = (String)getProperties().get("cm:name"); + + // if we didn't find it as a property get the name from the association name + if (this.name == null) + { + ChildAssociationRef parentRef = this.services.getNodeService().getPrimaryParent(this.nodeRef); + if (parentRef != null && parentRef.getQName() != null) + { + this.name = parentRef.getQName().getLocalName(); + } + else + { + this.name = ""; + } + } + } + + return this.name; + } + + /** + * @return The children of this Node as TemplateNode wrappers + */ + public List getChildren() + { + if (this.children == null) + { + List childRefs = this.services.getNodeService().getChildAssocs(this.nodeRef); + this.children = new ArrayList(childRefs.size()); + for (ChildAssociationRef ref : childRefs) + { + // create our Node representation from the NodeRef + TemplateNode child = new TemplateNode(ref.getChildRef(), this.services, this.imageResolver); + this.children.add(child); + } + } + + return this.children; + } + + /** + * @return A map capable of returning the TemplateNode at the specified Path as a child of this node. + */ + public Map getChildByNamePath() + { + return new NamePathResultsMap(this, this.services); + } + + /** + * @return A map capable of returning a List of TemplateNode objects from an XPath query + * as children of this node. + */ + public Map getChildrenByXPath() + { + return new XPathResultsMap(this, this.services); + } + + /** + * @return The associations for this Node. As a Map of assoc name to a List of TemplateNodes. + */ + public Map> getAssocs() + { + if (this.assocs == null) + { + List refs = this.services.getNodeService().getTargetAssocs(this.nodeRef, RegexQNamePattern.MATCH_ALL); + this.assocs = new QNameMap>(this.services.getNamespaceService()); + for (AssociationRef ref : refs) + { + String qname = ref.getTypeQName().toString(); + List nodes = assocs.get(qname); + if (nodes == null) + { + // first access for the list for this qname + nodes = new ArrayList(4); + this.assocs.put(ref.getTypeQName().toString(), nodes); + } + nodes.add( new TemplateNode(ref.getTargetRef(), this.services, this.imageResolver) ); + } + } + + return this.assocs; + } + + /** + * @return All the properties known about this node. + */ + public Map getProperties() + { + if (this.propsRetrieved == false) + { + Map props = this.services.getNodeService().getProperties(this.nodeRef); + + for (QName qname : props.keySet()) + { + Serializable propValue = props.get(qname); + if (propValue instanceof NodeRef) + { + // NodeRef object properties are converted to new TemplateNode objects + // so they can be used as objects within a template + propValue = new TemplateNode(((NodeRef)propValue), this.services, this.imageResolver); + } + else if (propValue instanceof ContentData) + { + // ContentData object properties are converted to TemplateContentData objects + // so the content and other properties of those objects can be accessed + propValue = new TemplateContentData((ContentData)propValue, qname); + } + this.properties.put(qname.toString(), propValue); + } + + this.propsRetrieved = true; + } + + return this.properties; + } + + /** + * @return true if this Node is a container (i.e. a folder) + */ + public boolean getIsContainer() + { + if (isContainer == null) + { + DictionaryService dd = this.services.getDictionaryService(); + isContainer = Boolean.valueOf( (dd.isSubClass(getType(), ContentModel.TYPE_FOLDER) == true && + dd.isSubClass(getType(), ContentModel.TYPE_SYSTEM_FOLDER) == false) ); + } + + return isContainer.booleanValue(); + } + + /** + * @return true if this Node is a Document (i.e. with content) + */ + public boolean getIsDocument() + { + if (isDocument == null) + { + DictionaryService dd = this.services.getDictionaryService(); + isDocument = Boolean.valueOf(dd.isSubClass(getType(), ContentModel.TYPE_CONTENT)); + } + + return isDocument.booleanValue(); + } + + /** + * @return The list of aspects applied to this node + */ + public Set getAspects() + { + if (this.aspects == null) + { + this.aspects = this.services.getNodeService().getAspects(this.nodeRef); + } + + return this.aspects; + } + + /** + * @param aspect The aspect name to test for + * + * @return true if the node has the aspect false otherwise + */ + public boolean hasAspect(String aspect) + { + if (this.aspects == null) + { + this.aspects = this.services.getNodeService().getAspects(this.nodeRef); + } + + if (aspect.startsWith(NAMESPACE_BEGIN)) + { + return aspects.contains((QName.createQName(aspect))); + } + else + { + boolean found = false; + for (QName qname : this.aspects) + { + if (qname.toPrefixString(this.services.getNamespaceService()).equals(aspect)) + { + found = true; + break; + } + } + return found; + } + } + + /** + * @return FreeMarker NodeModel for the XML content of this node, or null if no parsable XML found + */ + public NodeModel getXmlNodeModel() + { + try + { + return NodeModel.parse(new InputSource(new StringReader(getContent()))); + } + catch (Throwable err) + { + if (logger.isDebugEnabled()) + logger.debug(err.getMessage(), err); + + return null; + } + } + + /** + * @return Display path to this node + */ + public String getDisplayPath() + { + if (displayPath == null) + { + try + { + displayPath = this.services.getNodeService().getPath(this.nodeRef).toDisplayPath(this.services.getNodeService()); + } + catch (AccessDeniedException err) + { + displayPath = ""; + } + } + + return displayPath; + } + + /** + * @return the small icon image for this node + */ + public String getIcon16() + { + if (this.imageResolver != null) + { + if (getIsDocument()) + { + return this.imageResolver.resolveImagePathForName(getName(), true); + } + else + { + return "/images/icons/space_small.gif"; + } + } + else + { + return "/images/filetypes/_default.gif"; + } + } + + /** + * @return the large icon image for this node + */ + public String getIcon32() + { + if (this.imageResolver != null) + { + if (getIsDocument()) + { + return this.imageResolver.resolveImagePathForName(getName(), false); + } + else + { + String icon = (String)getProperties().get("app:icon"); + if (icon != null) + { + return "/images/icons/" + icon + ".gif"; + } + else + { + return "/images/icons/space-icon-default.gif"; + } + } + } + else + { + return "/images/filetypes32/_default.gif"; + } + } + + /** + * @return true if the node is currently locked + */ + public boolean getIsLocked() + { + boolean locked = false; + + if (getAspects().contains(ContentModel.ASPECT_LOCKABLE)) + { + LockStatus lockStatus = this.services.getLockService().getLockStatus(this.nodeRef); + if (lockStatus == LockStatus.LOCKED || lockStatus == LockStatus.LOCK_OWNER) + { + locked = true; + } + } + + return locked; + } + + /** + * @return the parent node + */ + public TemplateNode getParent() + { + if (parent == null) + { + NodeRef parentRef = this.services.getNodeService().getPrimaryParent(nodeRef).getParentRef(); + // handle root node (no parent!) + if (parentRef != null) + { + parent = new TemplateNode(parentRef, this.services, this.imageResolver); + } + } + + return parent; + } + + /** + * @return the content String for this node from the default content property + * (@see ContentModel.PROP_CONTENT) + */ + public String getContent() + { + ContentService contentService = this.services.getContentService(); + ContentReader reader = contentService.getReader(this.nodeRef, ContentModel.PROP_CONTENT); + return reader != null ? reader.getContentString() : ""; + } + + /** + * @return url to the content stream for this node for the default content property + * (@see ContentModel.PROP_CONTENT) + */ + public String getUrl() + { + try + { + return MessageFormat.format(CONTENT_DEFAULT_URL, new Object[] { + nodeRef.getStoreRef().getProtocol(), + nodeRef.getStoreRef().getIdentifier(), + nodeRef.getId(), + URLEncoder.encode(getName(), "US-ASCII") } ); + } + catch (UnsupportedEncodingException err) + { + throw new TemplateException("Failed to encode content URL for node: " + nodeRef, err); + } + } + + /** + * @return The mimetype encoding for content attached to the node from the default content property + * (@see ContentModel.PROP_CONTENT) + */ + public String getMimetype() + { + if (mimetype == null) + { + TemplateContentData content = (TemplateContentData)this.getProperties().get(ContentModel.PROP_CONTENT); + if (content != null) + { + mimetype = content.getMimetype(); + } + } + + return mimetype; + } + + /** + * @return The size in bytes of the content attached to the node from the default content property + * (@see ContentModel.PROP_CONTENT) + */ + public long getSize() + { + if (size == null) + { + TemplateContentData content = (TemplateContentData)this.getProperties().get(ContentModel.PROP_CONTENT); + if (content != null) + { + size = content.getSize(); + } + } + + return size != null ? size.longValue() : 0L; + } + + /** + * @return the image resolver instance used by this node + */ + public TemplateImageResolver getImageResolver() + { + return this.imageResolver; + } + + /** + * Override Object.toString() to provide useful debug output + */ + public String toString() + { + if (this.services.getNodeService().exists(nodeRef)) + { + return "Node Type: " + getType() + + "\nNode Properties: " + this.getProperties().toString() + + "\nNode Aspects: " + this.getAspects().toString(); + } + else + { + return "Node no longer exists: " + nodeRef; + } + } + + + /** + * Inner class wrapping and providing access to a ContentData property + */ + public class TemplateContentData implements Serializable + { + public TemplateContentData(ContentData contentData, QName property) + { + this.contentData = contentData; + this.property = property; + } + + public String getContent() + { + ContentService contentService = services.getContentService(); + ContentReader reader = contentService.getReader(nodeRef, property); + return reader != null ? reader.getContentString() : ""; + } + + public String getUrl() + { + try + { + return MessageFormat.format(CONTENT_PROP_URL, new Object[] { + nodeRef.getStoreRef().getProtocol(), + nodeRef.getStoreRef().getIdentifier(), + nodeRef.getId(), + URLEncoder.encode(getName(), "US-ASCII"), + URLEncoder.encode(property.toString(), "US-ASCII") } ); + } + catch (UnsupportedEncodingException err) + { + throw new TemplateException("Failed to encode content URL for node: " + nodeRef, err); + } + } + + public long getSize() + { + return contentData.getSize(); + } + + public String getMimetype() + { + return contentData.getMimetype(); + } + + private ContentData contentData; + private QName property; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateProcessor.java b/source/java/org/alfresco/service/cmr/repository/TemplateProcessor.java new file mode 100644 index 0000000000..2365ea6ce3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/TemplateProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Writer; + +/** + * Interface to be implemented by template engine wrapper classes. The developer is responsible + * for interfacing to an appropriate template engine, using the supplied data model as input to + * the template and directing the output to the Writer stream. + * + * @author Kevin Roast + */ +public interface TemplateProcessor +{ + /** + * Process a template against the supplied data model and write to the out. + * + * @param template Template name/path + * @param model Object model to process template against + * @param out Writer object to send output too + */ + public void process(String template, Object model, Writer out); +} diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateService.java b/source/java/org/alfresco/service/cmr/repository/TemplateService.java new file mode 100644 index 0000000000..35ae9ad106 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/TemplateService.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import java.io.Writer; + +/** + * Template Service. + *

    + * Provides an interface to services for executing template engine against a template file + * and data model. + *

    + * The service provides a configured list of available template engines. The template file + * can either be in the repository (passed as NodeRef string) or on the classpath. The data + * model is specified to the template engine. The FreeMarker template engine is used by default. + * + * @author Kevin Roast + */ +public interface TemplateService +{ + /** + * Process a template against the supplied data model and write to the out. + * + * @param engine Name of the template engine to use + * @param template Template (qualified classpath name or noderef) + * @param model Object model to process template against + * + * @return output of the template process as a String + */ + public String processTemplate(String engine, String template, Object model) + throws TemplateException; + + /** + * Process a template against the supplied data model and write to the out. + * + * @param engine Name of the template engine to use + * @param template Template (qualified classpath name or noderef) + * @param model Object model to process template against + * @param out Writer object to send output too + */ + public void processTemplate(String engine, String template, Object model, Writer out) + throws TemplateException; + + /** + * Return a TemplateProcessor instance for the specified engine name. + * Note that the processor instance is NOT thread safe! + * + * @param engine Name of the template engine to get or null for default + * + * @return TemplateProcessor + */ + public TemplateProcessor getTemplateProcessor(String engine); +} diff --git a/source/java/org/alfresco/service/cmr/repository/XPathException.java b/source/java/org/alfresco/service/cmr/repository/XPathException.java new file mode 100644 index 0000000000..65b9409b8b --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/XPathException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class XPathException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 3544955454552815923L; + + public XPathException(String msg) + { + super(msg); + } + + public XPathException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverter.java b/source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverter.java new file mode 100644 index 0000000000..c6e6383a0f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverter.java @@ -0,0 +1,705 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository.datatype; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Timestamp; +import java.text.DecimalFormat; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO8601DateFormat; + +/** + * Support for generic conversion between types. + * + * Additional conversions may be added. Basic interoperabikitynos supported. + * + * Direct conversion and two stage conversions via Number are supported. We do + * not support conversion by any route at the moment + * + * TODO: Add support for Path + * + * TODO: Add support for lucene + * + * TODO: Add suport to check of a type is convertable + * + * TODO: Support for dynamically managing conversions + * + * @author andyh + * + */ +public class DefaultTypeConverter +{ + + /** + * Default Type Converter + */ + public static TypeConverter INSTANCE = new TypeConverter(); + + /** + * Initialise default set of Converters + */ + static + { + + // + // From string + // + + INSTANCE.addConverter(String.class, Boolean.class, new TypeConverter.Converter() + { + public Boolean convert(String source) + { + return Boolean.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, Character.class, new TypeConverter.Converter() + { + public Character convert(String source) + { + if ((source == null) || (source.length() == 0)) + { + return null; + } + return Character.valueOf(source.charAt(0)); + } + }); + + INSTANCE.addConverter(String.class, Number.class, new TypeConverter.Converter() + { + public Number convert(String source) + { + try + { + return DecimalFormat.getNumberInstance().parse(source); + } + catch (ParseException e) + { + throw new TypeConversionException("Failed to parse number " + source, e); + } + } + }); + + INSTANCE.addConverter(String.class, Byte.class, new TypeConverter.Converter() + { + public Byte convert(String source) + { + return Byte.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, Short.class, new TypeConverter.Converter() + { + public Short convert(String source) + { + return Short.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, Integer.class, new TypeConverter.Converter() + { + public Integer convert(String source) + { + return Integer.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, Long.class, new TypeConverter.Converter() + { + public Long convert(String source) + { + return Long.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, Float.class, new TypeConverter.Converter() + { + public Float convert(String source) + { + return Float.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, Double.class, new TypeConverter.Converter() + { + public Double convert(String source) + { + return Double.valueOf(source); + } + }); + + INSTANCE.addConverter(String.class, BigInteger.class, new TypeConverter.Converter() + { + public BigInteger convert(String source) + { + return new BigInteger(source); + } + }); + + INSTANCE.addConverter(String.class, BigDecimal.class, new TypeConverter.Converter() + { + public BigDecimal convert(String source) + { + return new BigDecimal(source); + } + }); + + INSTANCE.addConverter(String.class, Date.class, new TypeConverter.Converter() + { + public Date convert(String source) + { + Date date = ISO8601DateFormat.parse(source); + if (date == null) + { + throw new TypeConversionException("Failed to parse date " + source); + } + return date; + } + }); + + INSTANCE.addConverter(String.class, Duration.class, new TypeConverter.Converter() + { + public Duration convert(String source) + { + return new Duration(source); + } + }); + + INSTANCE.addConverter(String.class, QName.class, new TypeConverter.Converter() + { + public QName convert(String source) + { + return QName.createQName(source); + } + }); + + INSTANCE.addConverter(String.class, ContentData.class, new TypeConverter.Converter() + { + public ContentData convert(String source) + { + return ContentData.createContentProperty(source); + } + + }); + + INSTANCE.addConverter(String.class, NodeRef.class, new TypeConverter.Converter() + { + public NodeRef convert(String source) + { + return new NodeRef(source); + } + + }); + + INSTANCE.addConverter(String.class, InputStream.class, new TypeConverter.Converter() + { + public InputStream convert(String source) + { + try + { + return new ByteArrayInputStream(source.getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) + { + throw new TypeConversionException("Encoding not supported", e); + } + } + }); + + + // + // Number to Subtypes and Date + // + + INSTANCE.addConverter(Number.class, Byte.class, new TypeConverter.Converter() + { + public Byte convert(Number source) + { + return Byte.valueOf(source.byteValue()); + } + }); + + INSTANCE.addConverter(Number.class, Short.class, new TypeConverter.Converter() + { + public Short convert(Number source) + { + return Short.valueOf(source.shortValue()); + } + }); + + INSTANCE.addConverter(Number.class, Integer.class, new TypeConverter.Converter() + { + public Integer convert(Number source) + { + return Integer.valueOf(source.intValue()); + } + }); + + INSTANCE.addConverter(Number.class, Long.class, new TypeConverter.Converter() + { + public Long convert(Number source) + { + return Long.valueOf(source.longValue()); + } + }); + + INSTANCE.addConverter(Number.class, Float.class, new TypeConverter.Converter() + { + public Float convert(Number source) + { + return Float.valueOf(source.floatValue()); + } + }); + + INSTANCE.addConverter(Number.class, Double.class, new TypeConverter.Converter() + { + public Double convert(Number source) + { + return Double.valueOf(source.doubleValue()); + } + }); + + INSTANCE.addConverter(Number.class, Date.class, new TypeConverter.Converter() + { + public Date convert(Number source) + { + return new Date(source.longValue()); + } + }); + + INSTANCE.addConverter(Number.class, String.class, new TypeConverter.Converter() + { + public String convert(Number source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(Number.class, BigInteger.class, new TypeConverter.Converter() + { + public BigInteger convert(Number source) + { + if (source instanceof BigDecimal) + { + return ((BigDecimal) source).toBigInteger(); + } + else + { + return BigInteger.valueOf(source.longValue()); + } + } + }); + + INSTANCE.addConverter(Number.class, BigDecimal.class, new TypeConverter.Converter() + { + public BigDecimal convert(Number source) + { + if (source instanceof BigInteger) + { + return new BigDecimal((BigInteger) source); + } + else + { + return BigDecimal.valueOf(source.longValue()); + } + } + }); + + INSTANCE.addDynamicTwoStageConverter(Number.class, String.class, InputStream.class); + + // + // Date, Timestamp -> + // + + INSTANCE.addConverter(Timestamp.class, Date.class, new TypeConverter.Converter() + { + public Date convert(Timestamp source) + { + return new Date(source.getTime()); + } + }); + + INSTANCE.addConverter(Date.class, Number.class, new TypeConverter.Converter() + { + public Number convert(Date source) + { + return Long.valueOf(source.getTime()); + } + }); + + INSTANCE.addConverter(Date.class, String.class, new TypeConverter.Converter() + { + public String convert(Date source) + { + return ISO8601DateFormat.format(source); + } + }); + + INSTANCE.addConverter(Date.class, Calendar.class, new TypeConverter.Converter() + { + public Calendar convert(Date source) + { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(source); + return calendar; + } + }); + + INSTANCE.addDynamicTwoStageConverter(Date.class, String.class, InputStream.class); + + // + // Boolean -> + // + + INSTANCE.addConverter(Boolean.class, String.class, new TypeConverter.Converter() + { + public String convert(Boolean source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Boolean.class, String.class, InputStream.class); + + // + // Character -> + // + + INSTANCE.addConverter(Character.class, String.class, new TypeConverter.Converter() + { + public String convert(Character source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Character.class, String.class, InputStream.class); + + // + // Duration -> + // + + INSTANCE.addConverter(Duration.class, String.class, new TypeConverter.Converter() + { + public String convert(Duration source) + { + return source.toString(); + } + + }); + + INSTANCE.addDynamicTwoStageConverter(Duration.class, String.class, InputStream.class); + + // + // Byte + // + + INSTANCE.addConverter(Byte.class, String.class, new TypeConverter.Converter() + { + public String convert(Byte source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Byte.class, String.class, InputStream.class); + + // + // Short + // + + INSTANCE.addConverter(Short.class, String.class, new TypeConverter.Converter() + { + public String convert(Short source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Short.class, String.class, InputStream.class); + + // + // Integer + // + + INSTANCE.addConverter(Integer.class, String.class, new TypeConverter.Converter() + { + public String convert(Integer source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Integer.class, String.class, InputStream.class); + + // + // Long + // + + INSTANCE.addConverter(Long.class, String.class, new TypeConverter.Converter() + { + public String convert(Long source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Long.class, String.class, InputStream.class); + + // + // Float + // + + INSTANCE.addConverter(Float.class, String.class, new TypeConverter.Converter() + { + public String convert(Float source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Float.class, String.class, InputStream.class); + + // + // Double + // + + INSTANCE.addConverter(Double.class, String.class, new TypeConverter.Converter() + { + public String convert(Double source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Double.class, String.class, InputStream.class); + + // + // BigInteger + // + + INSTANCE.addConverter(BigInteger.class, String.class, new TypeConverter.Converter() + { + public String convert(BigInteger source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(BigInteger.class, String.class, InputStream.class); + + // + // Calendar + // + + INSTANCE.addConverter(Calendar.class, Date.class, new TypeConverter.Converter() + { + public Date convert(Calendar source) + { + return source.getTime(); + } + }); + + INSTANCE.addConverter(Calendar.class, String.class, new TypeConverter.Converter() + { + public String convert(Calendar source) + { + return ISO8601DateFormat.format(source.getTime()); + } + }); + + // + // BigDecimal + // + + INSTANCE.addConverter(BigDecimal.class, String.class, new TypeConverter.Converter() + { + public String convert(BigDecimal source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(BigDecimal.class, String.class, InputStream.class); + + // + // QName + // + + INSTANCE.addConverter(QName.class, String.class, new TypeConverter.Converter() + { + public String convert(QName source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(QName.class, String.class, InputStream.class); + + // + // NodeRef + // + + INSTANCE.addConverter(NodeRef.class, String.class, new TypeConverter.Converter() + { + public String convert(NodeRef source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(NodeRef.class, String.class, InputStream.class); + + // + // ContentData + // + + INSTANCE.addConverter(ContentData.class, String.class, new TypeConverter.Converter() + { + public String convert(ContentData source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(ContentData.class, String.class, InputStream.class); + + // + // Path + // + + INSTANCE.addConverter(Path.class, String.class, new TypeConverter.Converter() + { + public String convert(Path source) + { + return source.toString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(Path.class, String.class, InputStream.class); + + // + // Content Reader + // + + INSTANCE.addConverter(ContentReader.class, InputStream.class, new TypeConverter.Converter() + { + public InputStream convert(ContentReader source) + { + return source.getContentInputStream(); + } + }); + + INSTANCE.addConverter(ContentReader.class, String.class, new TypeConverter.Converter() + { + public String convert(ContentReader source) + { + String encoding = source.getEncoding(); + if (encoding == null || !encoding.equals("UTF-8")) + { + throw new TypeConversionException("Cannot convert non UTF-8 streams to String."); + } + + // TODO: Throw error on size limit + + return source.getContentString(); + } + }); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, Date.class); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, Double.class); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, Long.class); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, Boolean.class); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, QName.class); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, Path.class); + + INSTANCE.addDynamicTwoStageConverter(ContentReader.class, String.class, NodeRef.class); + + // + // Input Stream + // + + INSTANCE.addConverter(InputStream.class, String.class, new TypeConverter.Converter() + { + public String convert(InputStream source) + { + try + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = source.read(buffer)) > 0) + { + out.write(buffer, 0, read); + } + byte[] data = out.toByteArray(); + return new String(data, "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + throw new TypeConversionException("Cannot convert input stream to String.", e); + } + catch (IOException e) + { + throw new TypeConversionException("Conversion from stream to string failed", e); + } + finally + { + if (source != null) + { + try { source.close(); } catch(IOException e) {}; + } + } + } + }); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, Date.class); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, Double.class); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, Long.class); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, Boolean.class); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, QName.class); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, Path.class); + + INSTANCE.addDynamicTwoStageConverter(InputStream.class, String.class, NodeRef.class); + + } + +} diff --git a/source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverterTest.java b/source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverterTest.java new file mode 100644 index 0000000000..e01295c735 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/datatype/DefaultTypeConverterTest.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository.datatype; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Date; + +import junit.framework.TestCase; + +import org.alfresco.util.ISO8601DateFormat; + +public class DefaultTypeConverterTest extends TestCase +{ + + public DefaultTypeConverterTest() + { + super(); + } + + public DefaultTypeConverterTest(String arg0) + { + super(arg0); + } + + public void testPrimitives() + { + assertEquals(Boolean.valueOf(false), DefaultTypeConverter.INSTANCE.convert(Boolean.class, false)); + assertEquals(Boolean.valueOf(true), DefaultTypeConverter.INSTANCE.convert(Boolean.class, true)); + assertEquals(Character.valueOf('a'), DefaultTypeConverter.INSTANCE.convert(Character.class, 'a')); + assertEquals(Byte.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Byte.class, (byte) 3)); + assertEquals(Short.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Short.class, (short) 4)); + assertEquals(Integer.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Integer.class, (int) 5)); + assertEquals(Long.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Long.class, (long) 6)); + assertEquals(Float.valueOf("7.1"), DefaultTypeConverter.INSTANCE.convert(Float.class, (float) 7.1)); + assertEquals(Double.valueOf("123.123"), DefaultTypeConverter.INSTANCE.convert(Double.class, (double) 123.123)); + } + + public void testNoConversion() + { + assertEquals(Boolean.valueOf(false), DefaultTypeConverter.INSTANCE.convert(Boolean.class, Boolean.valueOf(false))); + assertEquals(Boolean.valueOf(true), DefaultTypeConverter.INSTANCE.convert(Boolean.class, Boolean.valueOf(true))); + assertEquals(Character.valueOf('w'), DefaultTypeConverter.INSTANCE.convert(Character.class, Character.valueOf('w'))); + assertEquals(Byte.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Byte.valueOf("3"))); + assertEquals(Short.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Short.class, Short.valueOf("4"))); + assertEquals(Integer.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Integer.valueOf("5"))); + assertEquals(Long.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Long.class, Long.valueOf("6"))); + assertEquals(Float.valueOf("7.1"), DefaultTypeConverter.INSTANCE.convert(Float.class, Float.valueOf("7.1"))); + assertEquals(Double.valueOf("123.123"), DefaultTypeConverter.INSTANCE.convert(Double.class, Double.valueOf("123.123"))); + assertEquals(Double.valueOf("123.123"), DefaultTypeConverter.INSTANCE.convert(Double.class, Double.valueOf("123.123"))); + assertEquals(new BigInteger("1234567890123456789"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, new BigInteger("1234567890123456789"))); + assertEquals(new BigDecimal("12345678901234567890.12345678901234567890"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, new BigDecimal("12345678901234567890.12345678901234567890"))); + Date date = new Date(); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, date)); + assertEquals(new Duration("P25D"), DefaultTypeConverter.INSTANCE.convert(Duration.class, new Duration("P25D"))); + assertEquals("woof", DefaultTypeConverter.INSTANCE.convert(String.class, "woof")); + } + + public void testToString() + { + assertEquals("true", DefaultTypeConverter.INSTANCE.convert(String.class, new Boolean(true))); + assertEquals("false", DefaultTypeConverter.INSTANCE.convert(String.class, new Boolean(false))); + assertEquals("v", DefaultTypeConverter.INSTANCE.convert(String.class, Character.valueOf('v'))); + assertEquals("3", DefaultTypeConverter.INSTANCE.convert(String.class, Byte.valueOf("3"))); + assertEquals("4", DefaultTypeConverter.INSTANCE.convert(String.class, Short.valueOf("4"))); + assertEquals("5", DefaultTypeConverter.INSTANCE.convert(String.class, Integer.valueOf("5"))); + assertEquals("6", DefaultTypeConverter.INSTANCE.convert(String.class, Long.valueOf("6"))); + assertEquals("7.1", DefaultTypeConverter.INSTANCE.convert(String.class, Float.valueOf("7.1"))); + assertEquals("123.123", DefaultTypeConverter.INSTANCE.convert(String.class, Double.valueOf("123.123"))); + assertEquals("1234567890123456789", DefaultTypeConverter.INSTANCE.convert(String.class, new BigInteger("1234567890123456789"))); + assertEquals("12345678901234567890.12345678901234567890", DefaultTypeConverter.INSTANCE.convert(String.class, new BigDecimal("12345678901234567890.12345678901234567890"))); + Date date = new Date(); + assertEquals(ISO8601DateFormat.format(date), DefaultTypeConverter.INSTANCE.convert(String.class, date)); + assertEquals("P0Y25D", DefaultTypeConverter.INSTANCE.convert(String.class, new Duration("P0Y25D"))); + assertEquals("woof", DefaultTypeConverter.INSTANCE.convert(String.class, "woof")); + } + + public void testFromString() + { + assertEquals(Boolean.valueOf(true), DefaultTypeConverter.INSTANCE.convert(Boolean.class, "True")); + assertEquals(Boolean.valueOf(false), DefaultTypeConverter.INSTANCE.convert(Boolean.class, "woof")); + assertEquals(Character.valueOf('w'), DefaultTypeConverter.INSTANCE.convert(Character.class, "w")); + assertEquals(Byte.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Byte.class, "3")); + assertEquals(Short.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Short.class, "4")); + assertEquals(Integer.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Integer.class, "5")); + assertEquals(Long.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Long.class, "6")); + assertEquals(Float.valueOf("7.1"), DefaultTypeConverter.INSTANCE.convert(Float.class, "7.1")); + assertEquals(Double.valueOf("123.123"), DefaultTypeConverter.INSTANCE.convert(Double.class, "123.123")); + assertEquals(new BigInteger("1234567890123456789"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, "1234567890123456789")); + assertEquals(new BigDecimal("12345678901234567890.12345678901234567890"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, "12345678901234567890.12345678901234567890")); + assertEquals("2004-03-12T00:00:00.000Z", ISO8601DateFormat.format(DefaultTypeConverter.INSTANCE.convert(Date.class, "2004-03-12T00:00:00.000Z"))); + assertEquals(new Duration("P25D"), DefaultTypeConverter.INSTANCE.convert(Duration.class, "P25D")); + assertEquals("woof", DefaultTypeConverter.INSTANCE.convert(String.class, "woof")); + } + + public void testPrimativeAccessors() + { + assertEquals(false, DefaultTypeConverter.INSTANCE.convert(Boolean.class, false).booleanValue()); + assertEquals(true, DefaultTypeConverter.INSTANCE.convert(Boolean.class, true).booleanValue()); + assertEquals('a', DefaultTypeConverter.INSTANCE.convert(Character.class, 'a').charValue()); + assertEquals((byte) 3, DefaultTypeConverter.INSTANCE.convert(Byte.class, (byte) 3).byteValue()); + assertEquals((short) 4, DefaultTypeConverter.INSTANCE.convert(Short.class, (short) 4).shortValue()); + assertEquals((int) 5, DefaultTypeConverter.INSTANCE.convert(Integer.class, (int) 5).intValue()); + assertEquals((long) 6, DefaultTypeConverter.INSTANCE.convert(Long.class, (long) 6).longValue()); + assertEquals((float) 7.1, DefaultTypeConverter.INSTANCE.convert(Float.class, (float) 7.1).floatValue()); + assertEquals((double) 123.123, DefaultTypeConverter.INSTANCE.convert(Double.class, (double) 123.123).doubleValue()); + } + + public void testInterConversions() + { + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Byte.valueOf("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, Byte.valueOf("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Byte.valueOf("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, Byte.valueOf("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, Byte.valueOf("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, Byte.valueOf("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, Byte.valueOf("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, Byte.valueOf("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Short.valueOf("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, Short.valueOf("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Short.valueOf("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, Short.valueOf("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, Short.valueOf("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, Short.valueOf("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, Short.valueOf("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, Short.valueOf("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Integer.valueOf("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, Integer.valueOf("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Integer.valueOf("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, Integer.valueOf("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, Integer.valueOf("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, Integer.valueOf("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, Integer.valueOf("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, Integer.valueOf("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Long.valueOf("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, Long.valueOf("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Long.valueOf("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, Long.valueOf("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, Long.valueOf("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, Long.valueOf("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, Long.valueOf("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, Long.valueOf("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Float.valueOf("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, Float.valueOf("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Float.valueOf("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, Float.valueOf("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, Float.valueOf("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, Float.valueOf("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, Float.valueOf("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, Float.valueOf("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, Double.valueOf("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, Double.valueOf("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, Double.valueOf("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, Double.valueOf("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, Double.valueOf("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, Double.valueOf("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, Double.valueOf("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, Double.valueOf("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, new BigInteger("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, new BigInteger("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, new BigInteger("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, new BigInteger("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, new BigInteger("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, new BigInteger("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, new BigInteger("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, new BigInteger("8"))); + + assertEquals(Byte.valueOf("1"), DefaultTypeConverter.INSTANCE.convert(Byte.class, new BigDecimal("1"))); + assertEquals(Short.valueOf("2"), DefaultTypeConverter.INSTANCE.convert(Short.class, new BigDecimal("2"))); + assertEquals(Integer.valueOf("3"), DefaultTypeConverter.INSTANCE.convert(Integer.class, new BigDecimal("3"))); + assertEquals(Long.valueOf("4"), DefaultTypeConverter.INSTANCE.convert(Long.class, new BigDecimal("4"))); + assertEquals(Float.valueOf("5"), DefaultTypeConverter.INSTANCE.convert(Float.class, new BigDecimal("5"))); + assertEquals(Double.valueOf("6"), DefaultTypeConverter.INSTANCE.convert(Double.class, new BigDecimal("6"))); + assertEquals(new BigInteger("7"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, new BigDecimal("7"))); + assertEquals(new BigDecimal("8"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, new BigDecimal("8"))); + } + + public void testDate() + { + Date date = new Date(101); + + assertEquals(Byte.valueOf("101"), DefaultTypeConverter.INSTANCE.convert(Byte.class, date)); + assertEquals(Short.valueOf("101"), DefaultTypeConverter.INSTANCE.convert(Short.class, date)); + assertEquals(Integer.valueOf("101"), DefaultTypeConverter.INSTANCE.convert(Integer.class, date)); + assertEquals(Long.valueOf("101"), DefaultTypeConverter.INSTANCE.convert(Long.class, date)); + assertEquals(Float.valueOf("101"), DefaultTypeConverter.INSTANCE.convert(Float.class, date)); + assertEquals(Double.valueOf("101"), DefaultTypeConverter.INSTANCE.convert(Double.class, date)); + assertEquals(new BigInteger("101"), DefaultTypeConverter.INSTANCE.convert(BigInteger.class, date)); + assertEquals(new BigDecimal("101"), DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, date)); + + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (byte)101)); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (short)101)); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (int)101)); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (long)101)); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (float)101)); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (double)101)); + + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, new BigInteger("101"))); + assertEquals(date, DefaultTypeConverter.INSTANCE.convert(Date.class, (Object)(new BigDecimal("101")))); + + assertEquals(101, DefaultTypeConverter.INSTANCE.intValue(date)); + } + + public void testMultiValue() + { + ArrayList list = makeList(); + + assertEquals(true, DefaultTypeConverter.INSTANCE.isMultiValued(list)); + assertEquals(14, DefaultTypeConverter.INSTANCE.size(list)); + + for(String stringValue: DefaultTypeConverter.INSTANCE.getCollection(String.class, list)) + { + System.out.println("Value is "+stringValue); + } + + } + + private ArrayList makeList() + { + ArrayList list = new ArrayList(); + list.add(Boolean.valueOf(true)); + list.add(Boolean.valueOf(false)); + list.add(Character.valueOf('q')); + list.add(Byte.valueOf("1")); + list.add(Short.valueOf("2")); + list.add(Integer.valueOf("3")); + list.add(Long.valueOf("4")); + list.add(Float.valueOf("5")); + list.add(Double.valueOf("6")); + list.add(new BigInteger("7")); + list.add(new BigDecimal("8")); + list.add(new Date()); + list.add(new Duration("P5Y0M")); + list.add("Hello mum"); + return list; + } + + public void testSingleValuseAsMultiValue() + { + Integer integer = Integer.valueOf(43); + + assertEquals(false, DefaultTypeConverter.INSTANCE.isMultiValued(integer)); + assertEquals(1, DefaultTypeConverter.INSTANCE.size(integer)); + + for(String stringValue: DefaultTypeConverter.INSTANCE.getCollection(String.class, integer)) + { + System.out.println("Value is "+stringValue); + } + + } + + public void testNullAndEmpty() + { + assertNull(DefaultTypeConverter.INSTANCE.convert(Boolean.class, null)); + ArrayList list = new ArrayList(); + assertNotNull(DefaultTypeConverter.INSTANCE.convert(Boolean.class, list)); + list.add(null); + assertNotNull(DefaultTypeConverter.INSTANCE.convert(Boolean.class, list)); + + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/datatype/Duration.java b/source/java/org/alfresco/service/cmr/repository/datatype/Duration.java new file mode 100644 index 0000000000..cafad1aa0d --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/datatype/Duration.java @@ -0,0 +1,1001 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository.datatype; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StreamTokenizer; +import java.io.StringReader; +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.Date; + +import org.alfresco.util.CachingDateFormat; + +/** + * This data type represents duration/interval/period as defined by the XMLSchema type + * duration. + * + * The lexical representation of duration is + * PnYnMnDTnHnMnS. + * + * P is a literal value that starts the expression + * nY is an integer number of years followed by the literal Y + * nM is an integer number of months followed by the literal M + * nD is an integer number of days followed by the literal D + * T is the literal that separates the date and time + * nH is an integer number of hours followed by a literal H + * nM is an integer number of minutes followed by a literal M + * nS is a decimal number of seconds followed by a literal S + * + * Any numbers and designator may be absent if the value is zero. + * A minus sign may appear before the literal P to indicate a negative duration. + * If no time items are present the literal T must not appear. + * + * + * This implementation is immutable and thread safe. + * + * There are two forms of duration common on database types. + * The code contains warnings wheer these are relevant. + * + * @author andyh + */ +public class Duration implements Comparable, Serializable +{ + + static final long serialVersionUID = 3274526442325176068L; + + public static final String XML_DAY = "P1D"; + public static final String XML_WEEK = "P7D"; + public static final String XML_TWO_WEEKS = "P14D"; + public static final String XML_MONTH = "P1M"; + public static final String XML_QUARTER = "P3M"; + public static final String XML_SIX_MONTHS = "P6M"; + public static final String XML_YEAR = "P1Y"; + + public static final Duration DAY = new Duration(XML_DAY); + public static final Duration WEEK = new Duration(XML_WEEK); + public static final Duration TWO_WEEKS = new Duration(XML_TWO_WEEKS); + public static final Duration MONTH = new Duration(XML_MONTH); + public static final Duration QUARTER = new Duration(XML_QUARTER); + public static final Duration SIX_MONTHS = new Duration(XML_SIX_MONTHS); + public static final Duration YEAR = new Duration(XML_YEAR); + + private static final String s_parse = "-PYMDTHmS"; + + private boolean m_positive = true; + private int m_years = 0; + private int m_months = 0; + private int m_days = 0; + private int m_hours = 0; + private int m_mins = 0; + private int m_seconds = 0; + private int m_nanos = 0; + + // Date duration arithmetic + + /** + * Add a duration to a date and return the date plus the specified increment. + * + * @param date - the initial date + * @param duration - the duration to add on to the date (the duration may be negative) + * @return the adjusted date. + */ + public static Date add(Date date, Duration duration) + { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(Calendar.YEAR, (duration.m_positive ? 1 : -1) * duration.m_years); + c.add(Calendar.MONTH, (duration.m_positive ? 1 : -1) * duration.m_months); + c.add(Calendar.DATE, (duration.m_positive ? 1 : -1) * duration.m_days); + c.add(Calendar.HOUR_OF_DAY, (duration.m_positive ? 1 : -1) * duration.m_hours); + c.add(Calendar.MINUTE, (duration.m_positive ? 1 : -1) * duration.m_mins); + c.add(Calendar.SECOND, (duration.m_positive ? 1 : -1) * duration.m_seconds); + c.add(Calendar.MILLISECOND, (duration.m_positive ? 1 : -1) * duration.m_nanos / 1000000); + return c.getTime(); + } + + /** + * Subtract a period for a given date + * + * @param date - the intial date + * @param duration - the diration to subtract + * @return the adjusted date. + */ + + public static Date subtract(Date date, Duration duration) + { + return add(date, duration.unaryMinus()); + } + + + + /** + * Constructor for Duration - a zero value duration + */ + + public Duration() + { + super(); + } + + /** + * Construct a Duration from the XMLSchema definition + */ + + public Duration(String duration) + { + + if (duration.equals("P")) + { + throw new RuntimeException("Invalid period: P"); + } + + if (!duration.startsWith("P") && !duration.startsWith("-P")) + { + throw new RuntimeException("Invalid period: must start with P or -P"); + } + else + { + boolean dateMode = true; + int last = -1; + Double nval = null; + StringReader reader = new StringReader(duration); + StreamTokenizer tok = new StreamTokenizer(reader); + tok.resetSyntax(); + tok.eolIsSignificant(true); + tok.parseNumbers(); + tok.ordinaryChars('-', '-'); + tok.ordinaryChars('P', 'P'); + tok.ordinaryChars('Y', 'Y'); + tok.ordinaryChars('M', 'M'); + tok.ordinaryChars('D', 'D'); + tok.ordinaryChars('T', 'T'); + tok.ordinaryChars('H', 'H'); + tok.ordinaryChars('M', 'M'); + tok.ordinaryChars('S', 'S'); + + int token; + try + { + while ((token = tok.nextToken()) != StreamTokenizer.TT_EOF) + { + if (token == StreamTokenizer.TT_NUMBER) + { + nval = new Double(tok.nval); + } + else if (token == StreamTokenizer.TT_EOF) + { + throw new RuntimeException("Invalid EOF in Duration"); + } + else if (token == StreamTokenizer.TT_EOL) + { + throw new RuntimeException("Invalid EOL in Duration"); + } + else if (token == StreamTokenizer.TT_WORD) + { + throw new RuntimeException("Invalid text in Duration: " + tok.sval); + } + else + { + if (tok.ttype == '-') + { + last = checkIndex(last, "-"); + m_positive = false; + } + else if (tok.ttype == 'P') + { + last = checkIndex(last, "P"); + // nothing + } + else if (tok.ttype == 'Y') + { + last = checkIndex(last, "Y"); + if (nval != null) + { + m_years = nval.intValue(); + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + nval = null; + } + else if (tok.ttype == 'M') + { + if (dateMode) + { + last = checkIndex(last, "M"); + if (nval != null) + { + m_months = nval.intValue(); + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + nval = null; + } + else + { + last = checkIndex(last, "m"); + if (nval != null) + { + m_mins = nval.intValue(); + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + nval = null; + } + } + else if (tok.ttype == 'D') + { + last = checkIndex(last, "D"); + if (nval != null) + { + m_days = nval.intValue(); + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + nval = null; + } + else if (tok.ttype == 'T') + { + last = checkIndex(last, "T"); + dateMode = false; + nval = null; + } + else if (tok.ttype == 'H') + { + last = checkIndex(last, "H"); + if (nval != null) + { + m_hours = nval.intValue(); + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + nval = null; + } + else if (tok.ttype == 'S') + { + last = checkIndex(last, "S"); + if (nval != null) + { + m_seconds = nval.intValue(); + m_nanos = (int) ((long) (nval.doubleValue() * 1000000000) % 1000000000); + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + nval = null; + } + else + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + } + } + } + catch (IOException e) + { + throw new RuntimeException("IO Error parsing Duration: " + duration); + } + catch (RuntimeException e) + { + throw new RuntimeException("IO Error parsing Duration: " + duration, e); + } + } + } + + /** + * Simple index to check identifiers appear in order + */ + + private int checkIndex(int last, String search) + { + if ((search == null) || (search.length() == 0)) + { + throw new RuntimeException("Null or zero length serach"); + } + int index = s_parse.indexOf(search); + if (index > last) + { + return index; + } + else + { + throw new RuntimeException("Illegal position for identifier " + search); + } + } + + /** + * Create a duration given a date. The duration is between the two dates provided. + * + * Sadly, it works out the duration by incrementing the lower calendar until it matches + * the higher. + */ + + public Duration(Date date) + { + this(date, new Date()); + } + + /** + * Create a duration betweeen two dates expressed as strings. + * Uses the standard XML date form. + * + * @param start - the date at the start of the period + * @param end - the date at the end of the period + */ + + public Duration(String start, String end) + { + this(parseDate(start), parseDate(end)); + } + + + /** + * Helper method to parse eaets from strings + * @param stringDate + * @return + */ + private static Date parseDate(String stringDate) + { + DateFormat df = CachingDateFormat.getDateFormat(); + df.setLenient(true); + Date date; + + ParsePosition pp = new ParsePosition(0); + date = df.parse(stringDate, pp); + if ((pp.getIndex() < stringDate.length()) || (date == null)) + { + date = new Date(); + } + return date; + + } + + /** + * Construct a preiod between the two given dates + * + * @param start_in + * @param end_in + */ + public Duration(Date start_in, Date end_in) + { + boolean positive = true; + Date start; + Date end; + if (start_in.before(end_in)) + { + start = start_in; + end = end_in; + positive = true; + } + else + { + start = end_in; + end = start_in; + positive = false; + } + Calendar cstart = Calendar.getInstance(); + cstart.setTime(start); + Calendar cend = Calendar.getInstance(); + cend.setTime(end); + + int millis = cend.get(Calendar.MILLISECOND) - cstart.get(Calendar.MILLISECOND); + if (millis < 0) + { + millis += cstart.getActualMaximum(Calendar.MILLISECOND)+1; + } + cstart.add(Calendar.MILLISECOND, millis); + + int seconds = cend.get(Calendar.SECOND) - cstart.get(Calendar.SECOND); + if (seconds < 0) + { + seconds += cstart.getActualMaximum(Calendar.SECOND)+1; + } + cstart.add(Calendar.SECOND, seconds); + + int minutes = cend.get(Calendar.MINUTE) - cstart.get(Calendar.MINUTE); + if (minutes < 0) + { + minutes += cstart.getActualMaximum(Calendar.MINUTE)+1; + } + cstart.add(Calendar.MINUTE, minutes); + + int hours = cend.get(Calendar.HOUR_OF_DAY) - cstart.get(Calendar.HOUR_OF_DAY); + if (hours < 0) + { + hours += cstart.getActualMaximum(Calendar.HOUR_OF_DAY)+1; + } + cstart.add(Calendar.HOUR_OF_DAY, hours); + + int days = cend.get(Calendar.DAY_OF_MONTH) - cstart.get(Calendar.DAY_OF_MONTH); + if (days < 0) + { + days += cstart.getActualMaximum(Calendar.DAY_OF_MONTH)+1; + } + cstart.add(Calendar.DAY_OF_MONTH, days); + + int months = cend.get(Calendar.MONTH) - cstart.get(Calendar.MONTH); + if (months < 0) + { + months += cstart.getActualMaximum(Calendar.MONTH)+1; + } + cstart.add(Calendar.MONTH, months); + + int years = cend.get(Calendar.YEAR) - cstart.get(Calendar.YEAR); + //cstart.add(Calendar.YEAR, years); + + m_positive = positive; + m_years = years; + m_months = months; + m_days = days; + m_hours = hours; + m_mins = minutes; + m_seconds = seconds; + m_nanos = millis * 1000000; + + } + + /** + * Construct a duration from months seconds and nanos + * Checks sign and fixes up seconds and nano. + * Treats year-month abd day-sec as separate chunks + */ + + public Duration(boolean positive_in, long months_in, long seconds_in, long nanos_in) + { + + boolean positive = positive_in; + long months = months_in; + long seconds = seconds_in + nanos_in / 1000000000; + long nanos = nanos_in % 1000000000; + + // Fix up seconds and nanos to be of the same sign + + if ((seconds > 0) && (nanos < 0)) + { + seconds -= 1; + nanos += 1000000000; + } + else if ((seconds < 0) && (nanos > 0)) + { + seconds += 1; + nanos -= 1000000000; + } + + // seconds and nanos now the same sign - sum to test overall sign + + if ((months < 0) && (seconds + nanos < 0)) + { + // switch sign + positive = !positive; + months = -months; + seconds = -seconds; + nanos = -nanos; + } + else if ((months == 0) && (seconds + nanos < 0)) + { + // switch sign + positive = !positive; + months = -months; + seconds = -seconds; + nanos = -nanos; + } + else if ((months > 0) && (seconds + nanos < 0)) + { + throw new RuntimeException("Can not convert to period - incompatible signs for year_to_momth and day_to_second elements"); + } + else if ((months < 0) && (seconds + nanos > 0)) + { + throw new RuntimeException("Can not convert to period - incompatible signs for year_to_momth and day_to_second elements"); + } + else + { + // All +ve + } + + m_positive = positive; + m_years = (int) (months / 12); + m_months = (int) (months % 12); + + m_days = (int) (seconds / (3600 * 24)); + seconds -= m_days * 3600 * 24; + m_hours = (int) (seconds / 3600); + seconds -= m_hours * 3600; + m_mins = (int) (seconds / 60); + seconds -= m_mins * 60; + m_seconds = (int) seconds; + m_nanos = (int) nanos; + + } + + + // Duration arithmetic + + /** + * Add two durations together + */ + + public Duration add(Duration add) + { + + long months = (this.m_positive ? 1 : -1) * this.getTotalMonths() + (add.m_positive ? 1 : -1) * add.getTotalMonths(); + long seconds = (this.m_positive ? 1 : -1) * this.getTotalSeconds() + (add.m_positive ? 1 : -1) * add.getTotalSeconds(); + long nanos = (this.m_positive ? 1 : -1) * this.getTotalNanos() + (add.m_positive ? 1 : -1) * add.getTotalNanos(); + + Duration result = new Duration(true, months, seconds, nanos); + return result; + } + + /** + * Subtract one duration from another + */ + + public Duration subtract(Duration sub) + { + long months = (this.m_positive ? 1 : -1) * this.getTotalMonths() - (sub.m_positive ? 1 : -1) * sub.getTotalMonths(); + long seconds = (this.m_positive ? 1 : -1) * this.getTotalSeconds() - (sub.m_positive ? 1 : -1) * sub.getTotalSeconds(); + long nanos = (this.m_positive ? 1 : -1) * this.getTotalNanos() - (sub.m_positive ? 1 : -1) * sub.getTotalNanos(); + Duration result = new Duration(true, months, seconds, nanos); + return result; + } + + /** + * Negate the duration + */ + + public Duration unaryMinus() + { + Duration result = new Duration(!this.m_positive, this.getTotalMonths(), this.getTotalSeconds(), this.getTotalNanos()); + return result; + } + + /** + * Divide the duration - if year-month drops the day-second part of the duration + */ + + public Duration divide(int d) + { + if (isYearToMonth()) + { + long months = getTotalMonths(); + months /= d; + Duration result = new Duration(m_positive, months, 0, 0); + return result; + } + else + { + long seconds = getTotalSeconds(); + long nanos = (seconds * (1000000000 / d)) % 1000000000; + nanos += getTotalNanos() / d; + seconds /= d; + Duration result = new Duration(m_positive, 0, seconds, nanos); + return result; + } + } + + /** + * Helper method to get the total number of months - year-month + */ + + private long getTotalMonths() + { + return m_years * 12 + m_months; + } + + /** + * Helper method to get the total number of seconds + */ + + private long getTotalSeconds() + { + return m_seconds + m_mins * 60 + m_hours * 3600 + m_days * 3600 * 24; + } + + /** + * Helper method to get the total number of nanos (does not include seconds_ + */ + + private long getTotalNanos() + { + return m_nanos; + } + + /** + * Check if is year-month + */ + + public boolean isYearToMonth() + { + return (m_years != 0) || (m_months != 0); + } + + /** + * Check if is day-sec + */ + + public boolean isDayToSec() + { + return ((m_years == 0) && (m_months == 0)); + } + + /** + * Check if it includes time + */ + + public boolean hasTime() + { + return (m_hours != 0) || (m_mins != 0) || (m_seconds != 0) || (m_nanos != 0); + } + + /** + * Extract the year to month part + */ + + public Duration getYearToMonth() + { + Duration result = new Duration(m_positive, getTotalMonths(), 0, 0); + return result; + } + + /** + * Extract the day to sec part. + */ + + public Duration getDayToYear() + { + Duration result = new Duration(m_positive, 0, getTotalSeconds(), getTotalNanos()); + return result; + } + + /** + * Compare two durations + */ + + public int compareTo(Object o) + { + if (!(o instanceof Duration)) + { + throw new RuntimeException("Can not compare Duration and " + o.getClass().getName()); + } + + Duration d = (Duration) o; + if (this.m_positive != d.m_positive) + { + return (m_positive ? 1 : -1); + } + + if (this.getTotalMonths() != d.getTotalMonths()) + { + return (m_positive ? 1 : -1) * ((int) (this.getTotalMonths() - d.getTotalMonths())); + } + else if (this.getTotalSeconds() != d.getTotalSeconds()) + { + return (m_positive ? 1 : -1) * ((int) (this.getTotalSeconds() - d.getTotalSeconds())); + } + else if (this.getTotalNanos() != d.getTotalNanos()) + { + return (m_positive ? 1 : -1) * ((int) (this.getTotalNanos() - d.getTotalNanos())); + } + else + { + return 0; + } + } + + /** + * @see java.lang.Object#equals(Object) + */ + + public boolean equals(Object o) + { + if (this == o) + return true; + if (!(o instanceof Duration)) + return false; + Duration d = (Duration) o; + return (this.m_positive == d.m_positive) && (this.getTotalMonths() == d.getTotalMonths()) && (this.getTotalSeconds() == d.getTotalSeconds()) && (this.getTotalNanos() == d.getTotalNanos()); + + } + + /** + * @see java.lang.Object#hashCode() + */ + + public int hashCode() + { + int hash = 17; + hash = 37 * hash + (m_positive ? 1 : -1); + hash = 37 * hash + (int) getTotalMonths(); + hash = 37 * hash + (int) getTotalSeconds(); + hash = 37 * hash + (int) getTotalNanos(); + return hash; + } + + /** + * Produce the XML Schema string + * + * @see java.lang.Object#toString() + */ + + public String toString() + { + StringBuffer buffer = new StringBuffer(128); + if (!m_positive) + { + buffer.append("-"); + } + buffer.append("P"); + // Always include years as just P on its own is invalid + buffer.append(m_years).append("Y"); + + if (m_months != 0) + { + buffer.append(m_months).append("M"); + } + if (m_days != 0) + { + buffer.append(m_days).append("D"); + } + if (hasTime()) + { + buffer.append("T"); + if (m_hours != 0) + { + buffer.append(m_hours).append("H"); + } + if (m_mins != 0) + { + buffer.append(m_mins).append("M"); + } + if ((m_seconds != 0) || (m_nanos != 0)) + { + BigDecimal a = new BigDecimal(m_seconds); + BigDecimal b = new BigDecimal(m_nanos); + a = a.add(b.divide(new BigDecimal(1000000000), 9, BigDecimal.ROUND_HALF_EVEN)); + NumberFormat nf = NumberFormat.getInstance(); + buffer.append(nf.format(a)); + buffer.append("S"); + } + + } + + return buffer.toString(); + } + + /** + * Format in human readable form + * + * TODO: I18n + */ + + public String formattedString() + { + StringBuffer buffer = new StringBuffer(128); + if (!m_positive) + { + buffer.append("-"); + } + if (m_years != 0) + { + if (buffer.length() > 0) + buffer.append(" "); + buffer.append(m_years); + buffer.append((m_years == 1) ? " Year" : " Years"); + + } + if (m_months != 0) + { + if (buffer.length() > 0) + buffer.append(" "); + buffer.append(m_months); + buffer.append((m_months == 1) ? " Month" : " Months"); + } + if (m_days != 0) + { + if (buffer.length() > 0) + buffer.append(" "); + buffer.append(m_days); + buffer.append((m_days == 1) ? " Day" : " Days"); + } + if (hasTime()) + { + if (m_hours != 0) + { + if (buffer.length() > 0) + buffer.append(" "); + buffer.append(m_hours); + buffer.append((m_hours == 1) ? " Hour" : " Hours"); + } + if (m_mins != 0) + { + if (buffer.length() > 0) + buffer.append(" "); + buffer.append(m_mins); + buffer.append((m_mins == 1) ? " Minute" : " Minutes"); + } + if ((m_seconds != 0) || (m_nanos != 0)) + { + if (buffer.length() > 0) + buffer.append(" "); + BigDecimal a = new BigDecimal(m_seconds); + BigDecimal b = new BigDecimal(m_nanos); + a = a.add(b.divide(new BigDecimal(1000000000), 9, BigDecimal.ROUND_HALF_EVEN)); + NumberFormat nf = NumberFormat.getInstance(); + String formatted = nf.format(a); + buffer.append(formatted); + buffer.append(formatted.equals("1") ? " Second" : " Seconds"); + } + + } + + return buffer.toString(); + } + + + /** + * TODO: Tests that should be moved into a unit test + * + * @param args + */ + public static void main(String[] args) + { + Duration diff = new Duration("2002-04-02T01:01:01", "2003-03-01T00:00:00"); + System.out.println("Diff " + diff); + + try + { + Duration test = new Duration("P"); + System.out.println("Just P" + test); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + try + { + Duration test2 = new Duration("P Jones"); + System.out.println("P Jones" + test2); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + try + { + Duration test2 = new Duration("P12Y Jones"); + System.out.println("P Jones" + test2); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + try + { + Duration test = new Duration("PPPPPPPPPPPPPP"); + System.out.println("Just many P" + test); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + try + { + Duration test = new Duration("PY"); + System.out.println("PY" + test); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + try + { + Duration test = new Duration("PM"); + System.out.println("PM" + test); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + try + { + Duration test = new Duration("PP"); + System.out.println("PP" + test); + } + catch (RuntimeException e) + { + e.printStackTrace(); + } + + Date now = new Date(); + Calendar c = Calendar.getInstance(); + c.setTime(now); + c.add(Calendar.YEAR, -1); + c.add(Calendar.MONTH, +2); + c.add(Calendar.DAY_OF_MONTH, -3); + c.add(Calendar.HOUR_OF_DAY, +4); + c.add(Calendar.MINUTE, -5); + c.add(Calendar.SECOND, +6); + c.add(Calendar.MILLISECOND, -7); + + diff = new Duration(c.getTime(), now); + System.out.println("V: " + diff); + + Duration diff2 = new Duration(now, c.getTime()); + System.out.println("V: " + diff2); + + Duration a1 = new Duration("P2Y6M"); + Duration a2 = new Duration("P1DT2H3M1.5S"); + + Duration d = new Duration("P2Y6M5DT12H35M30.100S"); + System.out.println("V: " + d); + System.out.println("F: " + d.formattedString()); + System.out.println(" D: " + d.divide(2)); + System.out.println(" +: " + d.add(a1)); + System.out.println(" +: " + d.add(a1.add(a2))); + d = new Duration("P1DT2H3M1.5S"); + System.out.println("V: " + d); + System.out.println("F: " + d.formattedString()); + System.out.println(" D: " + d.divide(2)); + System.out.println(" +: " + d.add(a1)); + System.out.println(" +: " + d.add(a1.add(a2))); + d = new Duration("PT1.5S"); + System.out.println("V: " + d); + System.out.println("F: " + d.formattedString()); + System.out.println(" D: " + d.divide(2)); + System.out.println(" +: " + d.add(a1)); + System.out.println(" +: " + d.add(a1.add(a2))); + d = new Duration("P20M"); + System.out.println("V: " + d); + System.out.println("F: " + d.formattedString()); + System.out.println(" D: " + d.divide(2)); + System.out.println(" +: " + d.add(a1)); + System.out.println(" +: " + d.add(a1.add(a2))); + d = new Duration("P0Y20M0D"); + System.out.println("V: " + d); + System.out.println("F: " + d.formattedString()); + System.out.println(" D: " + d.divide(2)); + System.out.println(" +: " + d.add(a1)); + System.out.println(" +: " + d.add(a1.add(a2))); + d = new Duration("-P60D"); + System.out.println("V: " + d); + System.out.println("F: " + d.formattedString()); + System.out.println(" D: " + d.divide(10)); + System.out.println(" +: " + d.add(a2)); + //System.out.println(" +: " + d.add(a1)); + + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/datatype/TypeConversionException.java b/source/java/org/alfresco/service/cmr/repository/datatype/TypeConversionException.java new file mode 100644 index 0000000000..bcd9798dde --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/datatype/TypeConversionException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository.datatype; + + +/** + * Base Exception of Type Converter Exceptions. + * + * @author David Caruana + */ +public class TypeConversionException extends RuntimeException +{ + private static final long serialVersionUID = 3257008761007847733L; + + public TypeConversionException(String msg) + { + super(msg); + } + + public TypeConversionException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/service/cmr/repository/datatype/TypeConverter.java b/source/java/org/alfresco/service/cmr/repository/datatype/TypeConverter.java new file mode 100644 index 0000000000..d810f35207 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/datatype/TypeConverter.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.repository.datatype; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.util.ParameterCheck; + + +/** + * Support for generic conversion between types. + * + * Additional conversions may be added. + * + * Direct conversion and two stage conversions via Number are supported. We do + * not support conversion by any route at the moment + */ +public class TypeConverter +{ + + /** + * General conversion method to Object types (note it cannot support + * conversion to primary types due the restrictions of reflection. Use the + * static conversion methods to primitive types) + * + * @param propertyType - the target property type + * @param value - the value to be converted + * @return - the converted value as the correct type + */ + @SuppressWarnings("unchecked") + public final Object convert(DataTypeDefinition propertyType, Object value) + { + ParameterCheck.mandatory("Property type definition", propertyType); + + // Convert property type to java class + Class javaClass = null; + String javaClassName = propertyType.getJavaClassName(); + try + { + javaClass = Class.forName(javaClassName); + } + catch (ClassNotFoundException e) + { + throw new DictionaryException("Java class " + javaClassName + " of property type " + propertyType.getName() + " is invalid", e); + } + + return convert(javaClass, value); + } + + /** + * General conversion method to Object types (note it cannot support + * conversion to primary types due the restrictions of reflection. Use the + * static conversion methods to primitive types) + * + * @param The target type for the result of the conversion + * @param c - a class for the target type + * @param value - the value to be converted + * @return - the converted value as the correct type + * @throws TypeConversionException if the conversion cannot be performed + */ + @SuppressWarnings("unchecked") + public final T convert(Class c, Object value) + { + if(value == null) + { + return null; + } + + // Primative types + if (c.isPrimitive()) + { + // We can not suport primitive type conversion + throw new TypeConversionException("Can not convert direct to primitive type " + c.getName()); + } + + // Check if we already have the correct type + if (c.isInstance(value)) + { + return c.cast(value); + } + + // Find the correct conversion - if available and do the converiosn + Converter converter = getConverter(value, c); + if (converter == null) + { + throw new TypeConversionException( + "There is no conversion registered for the value: \n" + + " value class: " + value.getClass().getName() + "\n" + + " to class: " + c.getName() + "\n" + + " value: " + value.toString()); + } + + return (T) converter.convert(value); + } + + /** + * General conversion method to convert collection contents to the specified + * type. + * + * @param propertyType - the target property type + * @param value - the value to be converted + * @return - the converted value as the correct type + * @throws DictionaryException if the property type's registered java class is invalid + * @throws TypeConversionException if the conversion cannot be performed + */ + @SuppressWarnings("unchecked") + public final Collection convert(DataTypeDefinition propertyType, Collection values) + { + ParameterCheck.mandatory("Property type definition", propertyType); + + // Convert property type to java class + Class javaClass = null; + String javaClassName = propertyType.getJavaClassName(); + try + { + javaClass = Class.forName(javaClassName); + } + catch (ClassNotFoundException e) + { + throw new DictionaryException("Java class " + javaClassName + " of property type " + propertyType.getName() + " is invalid", e); + } + + return convert(javaClass, values); + } + + /** + * General conversion method to convert collection contents to the specified + * type. + * + * @param The target type for the result of the conversion + * @param c - a class for the target type + * @param value - the collection to be converted + * @return - the converted collection + * @throws TypeConversionException if the conversion cannot be performed + */ + public final Collection convert(Class c, Collection values) + { + if(values == null) + { + return null; + } + + Collection converted = new ArrayList(values.size()); + for (Object value : values) + { + converted.add(convert(c, value)); + } + + return converted; + } + + /** + * Get the boolean value for the value object + * May have conversion failure + * + * @param value + * @return + */ + public final boolean booleanValue(Object value) + { + return convert(Boolean.class, value).booleanValue(); + } + + /** + * Get the char value for the value object + * May have conversion failure + * + * @param value + * @return + */ + public final char charValue(Object value) + { + return convert(Character.class, value).charValue(); + } + + /** + * Get the byte value for the value object + * May have conversion failure + * + * @param value + * @return + */ + public final byte byteValue(Object value) + { + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + return convert(Byte.class, value).byteValue(); + } + + /** + * Get the short value for the value object + * May have conversion failure + * + * @param value + * @return + */ + public final short shortValue(Object value) + { + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + return convert(Short.class, value).shortValue(); + } + + /** + * Get the int value for the value object + * May have conversion failure + * + * @param value + * @return + */ + public final int intValue(Object value) + { + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + return convert(Integer.class, value).intValue(); + } + + /** + * Get the long value for the value object + * May have conversion failure + * + * @param value + * @return + */ + public final long longValue(Object value) + { + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + return convert(Long.class, value).longValue(); + } + + /** + * Get the bollean value for the value object + * May have conversion failure + * + * @param float + * @return + */ + public final float floatValue(Object value) + { + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + return convert(Float.class, value).floatValue(); + } + + /** + * Get the bollean value for the value object + * May have conversion failure + * + * @param double + * @return + */ + public final double doubleValue(Object value) + { + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + return convert(Double.class, value).doubleValue(); + } + + /** + * Is the value multi valued + * + * @param value + * @return true - if the underlyinf is a collection of values and not a singole value + */ + public final boolean isMultiValued(Object value) + { + return (value instanceof Collection); + } + + /** + * Get the number of values represented + * + * @param value + * @return 1 for normal values and the size of the collection for MVPs + */ + public final int size(Object value) + { + if (value instanceof Collection) + { + return ((Collection) value).size(); + } + else + { + return 1; + } + } + + /** + * Get a collection for the passed value + * + * @param value + * @return + */ + private final Collection createCollection(Object value) + { + Collection coll; + if (isMultiValued(value)) + { + coll = (Collection) value; + } + else + { + ArrayList list = new ArrayList(1); + list.add(value); + coll = list; + } + return coll; + } + + /** + * Get a collection for the passed value converted to the specified type + * + * @param c + * @param value + * @return + */ + public final Collection getCollection(Class c, Object value) + { + Collection coll = createCollection(value); + return convert(c, coll); + } + + /** + * Add a converter to the list of those available + * + * @param + * @param + * @param source + * @param destination + * @param converter + */ + public final void addConverter(Class source, Class destination, Converter converter) + { + Map map = conversions.get(source); + if (map == null) + { + map = new HashMap(); + conversions.put(source, map); + } + map.put(destination, converter); + } + + /** + * Add a dynamic two stage converter + * @param from + * @param intermediate + * @param to + * @param source + * @param intermediate + * @param destination + */ + public final Converter addDynamicTwoStageConverter(Class source, Class intermediate, Class destination) + { + Converter converter = new TypeConverter.DynamicTwoStageConverter(source, intermediate, destination); + addConverter(source, destination, converter); + return converter; + } + + /** + * Find conversion for the specified object + * + * Note: Takes into account the class of the object and any interfaces it may + * also support. + * + * @param + * @param + * @param source + * @param dest + * @return + */ + @SuppressWarnings("unchecked") + public final Converter getConverter(Object value, Class dest) + { + Converter converter = null; + if (value == null) + { + return null; + } + + // find via class of value + Class valueClass = value.getClass(); + converter = getConverter(valueClass, dest); + if (converter != null) + { + return converter; + } + + // find via supported interfaces of value + do + { + Class[] ifClasses = valueClass.getInterfaces(); + for (Class ifClass : ifClasses) + { + converter = getConverter(ifClass, dest); + if (converter != null) + { + return converter; + } + } + valueClass = valueClass.getSuperclass(); + } + while (valueClass != null); + + return null; + } + + /** + * Find a conversion for a specific Class + * + * @param + * @param + * @param source + * @param dest + * @return + */ + public Converter getConverter(Class source, Class dest) + { + Converter converter = null; + Class clazz = source; + do + { + Map map = conversions.get(clazz); + if (map == null) + { + continue; + } + converter = map.get(dest); + + if (converter == null) + { + // attempt to establish converter from source to dest via Number + Converter first = map.get(Number.class); + Converter second = null; + if (first != null) + { + map = conversions.get(Number.class); + if (map != null) + { + second = map.get(dest); + } + } + if (second != null) + { + converter = new TwoStageConverter(first, second); + } + } + } + while ((converter == null) && ((clazz = clazz.getSuperclass()) != null)); + + return converter; + } + + /** + * Map of conversion + */ + private Map> conversions = new HashMap>(); + + + // Support for pluggable conversions + + /** + * Conversion interface + * + * @author andyh + * + * @param From type + * @param To type + */ + public interface Converter + { + public T convert(F source); + } + + /** + * Support for chaining conversions + * + * @author andyh + * + * @param From Type + * @param Intermediate type + * @param To Type + */ + public static class TwoStageConverter implements Converter + { + Converter first; + + Converter second; + + TwoStageConverter(Converter first, Converter second) + { + this.first = first; + this.second = second; + } + + @SuppressWarnings("unchecked") + public T convert(F source) + { + return (T) second.convert((I) first.convert(source)); + } + } + + /** + * Support for chaining conversions + * + * @author David Caruana + * + * @param From Type + * @param Intermediate type + * @param To Type + */ + protected class DynamicTwoStageConverter implements Converter + { + Class from; + Class intermediate; + Class to; + + DynamicTwoStageConverter(Class from, Class intermediate, Class to) + { + this.from = from; + this.intermediate = intermediate; + this.to = to; + } + + /** + * @return from class + */ + public Class getFrom() + { + return from; + } + + /** + * @return intermediate class + */ + public Class getIntermediate() + { + return intermediate; + } + + /** + * @return to class + */ + public Class getTo() + { + return to; + } + + @SuppressWarnings("unchecked") + public T convert(F source) + { + Converter iConverter = TypeConverter.this.getConverter(from, intermediate); + Converter tConverter = TypeConverter.this.getConverter(intermediate, to); + if (iConverter == null || tConverter == null) + { + throw new TypeConversionException("Cannot convert from " + from.getName() + " to " + to.getName()); + } + + Object iValue = iConverter.convert(source); + Object tValue = tConverter.convert(iValue); + return (T)tValue; + } + } + +} diff --git a/source/java/org/alfresco/service/cmr/rule/Rule.java b/source/java/org/alfresco/service/cmr/rule/Rule.java new file mode 100644 index 0000000000..f358856fa4 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rule/Rule.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +package org.alfresco.service.cmr.rule; + +import org.alfresco.service.cmr.action.CompositeAction; + + +/** + * Rule Interface + * + * @author Roy Wetherall + */ +public interface Rule extends CompositeAction +{ + /** + * Indicates that the rule is applied to the children of the associated + * node, not just the node itself. + *

    + * By default this will be set to false. + * + * @return true if the rule is applied to the children of the associated node, + * false otherwise + */ + boolean isAppliedToChildren(); + + /** + * Set whether the rule is applied to all children of the associated node + * rather than just the node itself. + * + * @param isAppliedToChildren true if the rule should be applied to the children, false + * otherwise + */ + void applyToChildren(boolean isAppliedToChildren); + + /** + * Get the rule type name + * + * @return the rule type name + */ + String getRuleTypeName(); + } \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/rule/RuleService.java b/source/java/org/alfresco/service/cmr/rule/RuleService.java new file mode 100644 index 0000000000..ed3b94ef50 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rule/RuleService.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.rule; + +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Rule service interface. + * + * @author Roy Wetherall + */ +public interface RuleService +{ + /** + * Get the rule types currently defined in the repository. + * + * @return a list of rule types + */ + public List getRuleTypes(); + + /** + * Gets a rule type by name. + * + * @param name the name of the rule type + * @return the rule type, null if not found + */ + public RuleType getRuleType(String name); + + /** + * Indicates wether the rules for a given node are enabled or not. If the + * rules are not enabled then they will not be executed. + * + * @param nodeRef the node reference + * @return true if the rules are enabled, false otherwise + */ + public boolean rulesEnabled(NodeRef nodeRef); + + /** + * Disables the rules for a given node reference. When the rules are disabled they + * will not execute. + * + * @param nodeRef the node reference + */ + public void disableRules(NodeRef nodeRef); + + /** + * Enables the rules for a given node reference. When the rules are enabled they + * will execute as usual. By default all rules are enabled. + * + * @param nodeRef the node reference + */ + public void enableRules(NodeRef nodeRef); + + /** + * Disables a rule, preventing it from being fired. + * + * @param rule the rule to disable + */ + public void disableRule(Rule rule); + + /** + * Enables a rule previously disabled. + * + * @param rule the rule to enable + */ + public void enableRule(Rule rule); + + /** + * Indicates whether the node in question has any rules associated with it. + * + * @param nodeRef the node reference + * @return true if the node has rules associated, false otherwise + */ + public boolean hasRules(NodeRef nodeRef); + + /** + * Get all the rules associated with an actionable node, including those + * inherited from parents. + *

    + * An exception is raised if the actionable aspect is not present on the + * passed node. + * + * @param nodeRef the node reference + * @return a list of the rules associated with the node + */ + public List getRules(NodeRef nodeRef); + + /** + * Get the rules associated with an actionable node. + *

    + * Optionally this list includes rules inherited from its parents. + *

    + * An exception is raised if the actionable aspect is not present on the + * passed node. + * + * @param nodeRef the node reference + * @param includeInhertied indicates whether the inherited rules should be included in + * the result list or not + * @return a list of the rules associated with the node + */ + public List getRules(NodeRef nodeRef, boolean includeInhertied); + + /** + * Get the rules associatied with an actionable node that are of a specific rule type. + * + * @param nodeRef the node reference + * @param includeInhertiedRuleType indicates whether the inherited rules should be included in + * the result list or not + * @param ruleTypeName the name of the rule type, if null is passed all rule types + * are returned + * @return a list of the rules associated with the node + */ + public List getRules(NodeRef nodeRef, boolean includeInhertiedRuleType, String ruleTypeName); + + /** + * Get the rule given its id. + * + * @param nodeRef the node reference + * @param ruleId the rule id + * @return the rule corresponding ot the id + */ + public Rule getRule(NodeRef nodeRef, String ruleId); + + /** + * Helper method to create a new rule. + *

    + * Call add rule once the details of the rule have been specified in order + * to associate the rule with a node reference. + * + * @param ruleTypeName the name of the rule type + * @return the created rule + */ + public Rule createRule(String ruleTypeName); + + /** + * Saves the details of the rule to the specified node reference. + *

    + * If the rule is already associated with the node, the details are updated + * with those specified. + * + * @param nodeRef + * @param rule + */ + public void saveRule(NodeRef nodeRef, Rule rule); + + /** + * Removes a rule from the given rule actionable node + * + * @param nodeRef the actionable node reference + */ + public void removeRule(NodeRef nodeRef, Rule rule); + + /** + * Removes all the rules associated with an actionable node + * + * @param nodeRef the actionable node reference + */ + public void removeAllRules(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/rule/RuleServiceException.java b/source/java/org/alfresco/service/cmr/rule/RuleServiceException.java new file mode 100644 index 0000000000..121b240239 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rule/RuleServiceException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.rule; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Rule Service Exception Class + * + * @author Roy Wetherall + */ +public class RuleServiceException extends AlfrescoRuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = 3257571685241467958L; + + /** + * Construtor + * + * @param message the message string + */ + public RuleServiceException(String message) + { + super(message); + } + + /** + * Constructor + * + * @param message the message string + * @param source the source exception + */ + public RuleServiceException(String message, Throwable source) + { + super(message, source); + } +} diff --git a/source/java/org/alfresco/service/cmr/rule/RuleType.java b/source/java/org/alfresco/service/cmr/rule/RuleType.java new file mode 100644 index 0000000000..46ae8387b9 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rule/RuleType.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.rule; + +import org.alfresco.service.cmr.repository.NodeRef; + + +/** + * Rule type interface. + * + * @author Roy Wetherall + */ +public interface RuleType +{ + /** + * Some rule type constants + */ + public static final String INBOUND = "inbound"; + public static final String OUTGOING = "outgoing"; + + /** + * Get the name of the rule type. + *

    + * The name is unique and is used to identify the rule type. + * + * @return the name of the rule type + */ + public String getName(); + + /** + * Get the display label of the rule type. + * + * @return the display label + */ + public String getDisplayLabel(); + + /** + * Trigger the rules of the rule type for the node on the actioned upon node. + * + * @param nodeRef the node ref whos rule of rule type are to be triggered + * @param actionedUponNodeRef the node ref that the triggered rule will action upon + */ + public void triggerRuleType(NodeRef nodeRef, NodeRef actionedUponNodeRef); +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/search/CategoryService.java b/source/java/org/alfresco/service/cmr/search/CategoryService.java new file mode 100644 index 0000000000..d77978b4da --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/CategoryService.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import java.util.Collection; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + +/** + * Category Service + * + * The service for querying and creating categories. + * All other management can be carried out using the node service. + * + * Classification - the groupings of categories. There is a one-to-one mapping with aspects. For example, Region. + * Root Category - the top level categories in a classification. For example, Northern Europe + * Category - any other category below a root category + * + * @author Andy Hind + * + */ +public interface CategoryService +{ + /** + * Enumeration for navigation control. + * + * MEMBERS - get only category members (the things that have been classified in a category, not the sub categories) + * SUB_CATEGORIES - get sub categories only, not the things that hyave been classified. + * ALL - get both of the above + */ + public enum Mode {MEMBERS, SUB_CATEGORIES, ALL}; + + /** + * Depth from which to get nodes. + * + * IMMEDIATE - only immediate sub categories or members + * ANY - find subcategories or members at any level + */ + public enum Depth {IMMEDIATE, ANY}; + + /** + * Get the children of a given category node + * + * @param categoryRef - the category node + * @param mode - the enumeration mode for what to recover + * @param depth - the enumeration depth for what level to recover + * @return a collection of all the nodes found identified by their ChildAssocRef's + */ + public Collection getChildren(NodeRef categoryRef, Mode mode, Depth depth ); + + /** + * Get a list of all the categories appropriate for a given property. + * The full list of categories that may be assigned for this aspect. + * + * @param aspectQName + * @param depth - the enumeration depth for what level to recover + * @return a collection of all the nodes found identified by their ChildAssocRef's + */ + public Collection getCategories(StoreRef storeRef, QName aspectQName, Depth depth ); + + /** + * Get all the classification entries + * + * @return + */ + public Collection getClassifications(StoreRef storeRef); + + /** + * Get the root categories for an aspect/classification + * + * @param storeRef + * @param aspectName + * @return + */ + public Collection getRootCategories(StoreRef storeRef, QName aspectName); + + /** + * Get all the types that represent categories + * + * @return + */ + public Collection getClassificationAspects(); + + /** + * Create a new category. + * + * This will extend the category types in the data dictionary + * All it needs is the type name and the attribute in which to store noderefs to categories. + * + * @param aspectName + * @param attributeName + */ + public NodeRef createClassifiction(StoreRef storeRef, QName aspectName, String attributeName); + + /** + * Create a new root category in the given classification + * + * @param storeRef + * @param aspectName + * @param name + * @return + */ + public NodeRef createRootCategory(StoreRef storeRef, QName aspectName, String name); + + /** + * Create a new category. + * + * @param parent + * @param name + * @return + */ + public NodeRef createCategory(NodeRef parent, String name); + + /** + * Delete a classification + * + * @param storeRef + * @param aspectName + */ + public void deleteClassification(StoreRef storeRef, QName aspectName); + + /** + * Delete a category + * + * @param nodeRef + */ + public void deleteCategory(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/search/NamedQueryParameterDefinition.java b/source/java/org/alfresco/service/cmr/search/NamedQueryParameterDefinition.java new file mode 100644 index 0000000000..c5e297cb24 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/NamedQueryParameterDefinition.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import org.alfresco.service.namespace.QName; + +public interface NamedQueryParameterDefinition +{ + + /** + * Get the name of this parameter. It could be used as the well known name for the parameter. + * + * Not null + * + * @return + */ + public QName getQName(); + + /** + * Get the query parameter definition + * @return + */ + public QueryParameterDefinition getQueryParameterDefinition(); +} diff --git a/source/java/org/alfresco/service/cmr/search/QueryParameter.java b/source/java/org/alfresco/service/cmr/search/QueryParameter.java new file mode 100644 index 0000000000..f0f73e9dd7 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/QueryParameter.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import java.io.Serializable; + +import org.alfresco.service.namespace.QName; + +/** + * Encapsulates a query parameter + * + * @author andyh + * + */ +public class QueryParameter +{ + private QName qName; + + private Serializable value; + + public QueryParameter(QName qName, Serializable value) + { + this.qName = qName; + this.value = value; + } + + public QName getQName() + { + return qName; + } + + + public Serializable getValue() + { + return value; + } + + + + +} diff --git a/source/java/org/alfresco/service/cmr/search/QueryParameterDefinition.java b/source/java/org/alfresco/service/cmr/search/QueryParameterDefinition.java new file mode 100644 index 0000000000..c7efd5c709 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/QueryParameterDefinition.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; + +public interface QueryParameterDefinition extends NamedQueryParameterDefinition +{ + /** + * This parameter may apply to a well known property type. + * + * May be null + * + * @return + */ + public PropertyDefinition getPropertyDefinition(); + + /** + * Get the property type definition for this parameter. + * It could come from the property type definition if there is one + * + * Not null + * + * @return + */ + public DataTypeDefinition getDataTypeDefinition(); + + /** + * Get the default value for this parameter. + * + * @return + */ + public String getDefault(); + + /** + * Has this parameter got a default value? + * + * @return + */ + public boolean hasDefaultValue(); +} diff --git a/source/java/org/alfresco/service/cmr/search/ResultSet.java b/source/java/org/alfresco/service/cmr/search/ResultSet.java new file mode 100644 index 0000000000..720f30a9d0 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/ResultSet.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import java.util.List; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; + +/** + * An iterable result set from a searcher query. TODO: Expose meta data and XML + * + * @author andyh + * + */ +public interface ResultSet extends Iterable // Specfic iterator + // over + // ResultSetRows +{ + /** + * Get the relative paths to all the elements contained in this result set + */ + Path[] getPropertyPaths(); + + /** + * Get the size of the result set + */ + int length(); + + /** + * Get the id of the node at the given index + */ + NodeRef getNodeRef(int n); + + /** + * Get the score for the node at the given position + */ + float getScore(int n); + + /** + * Generate the XML form of this result set + */ + // Dom getXML(int page, int pageSize, boolean includeMetaData); + /** + * Generate as XML for Reading + */ + // Stream getStream(int page, int pageSize, boolean includeMetaData); + /** + * toString() as above but for the whole set + */ + // String toString(); + // ResultSetMetaData getMetaData(); + + void close(); + + ResultSetRow getRow(int i); + + List getNodeRefs(); + + List getChildAssocRefs(); + + ChildAssociationRef getChildAssocRef(int n); +} diff --git a/source/java/org/alfresco/service/cmr/search/ResultSetRow.java b/source/java/org/alfresco/service/cmr/search/ResultSetRow.java new file mode 100644 index 0000000000..6db782b7f6 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/ResultSetRow.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; + +/** + * A row in a result set + * + * TODO: Support for other non attribute features such as parents and path + * + * @author andyh + * + */ +public interface ResultSetRow +{ + /** + * Get the values of all available node properties + * + * @return + */ + public Map getValues(); + + /** + * Get a node property by path + * + * @param path + * @return + */ + public Serializable getValue(Path path); + + /** + * Get a node value by name + * + * @param qname + * @return + */ + public Serializable getValue(QName qname); + + /** + * The refernce to the node that equates to this row in the result set + * + * @return + */ + public NodeRef getNodeRef(); + + /** + * Get the score for this row in the result set + * + * @return + */ + public float getScore(); // Score is score + rank + potentially other + // stuff + + /** + * Get the containing result set + * + * @return + */ + public ResultSet getResultSet(); + + /** + * Return the QName of the node in the context in which it was found. + * @return + */ + + public QName getQName(); + + /** + * Get the position of this row in the containing set. + * @return + */ + public int getIndex(); + + /** + * Return the child assoc ref for this row + * @return + */ + public ChildAssociationRef getChildAssocRef(); + +} diff --git a/source/java/org/alfresco/service/cmr/search/SearchParameters.java b/source/java/org/alfresco/service/cmr/search/SearchParameters.java new file mode 100644 index 0000000000..dfd068358a --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/SearchParameters.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import java.util.ArrayList; + +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; + +/** + * This class provides parameters to define a search. + * + * @author Andy Hind + */ +public class SearchParameters extends SearchStatement +{ + public static final SortDefinition SORT_IN_DOCUMENT_ORDER_ASCENDING = new SortDefinition(SortDefinition.SortType.DOCUMENT, null, true); + public static final SortDefinition SORT_IN_DOCUMENT_ORDER_DESCENDING = new SortDefinition(SortDefinition.SortType.DOCUMENT, null, false); + public static final SortDefinition SORT_IN_SCORE_ORDER_ASCENDING = new SortDefinition(SortDefinition.SortType.SCORE, null, false); + public static final SortDefinition SORT_IN_SCORE_ORDER_DESCENDING = new SortDefinition(SortDefinition.SortType.SCORE, null, true); + + public enum Operator + { + OR, AND + } + + public static final Operator OR = Operator.OR; + public static final Operator AND = Operator.AND; + + private ArrayList stores = new ArrayList(1); + private ArrayList attributePaths = new ArrayList(1); + private ArrayList queryParameterDefinitions = new ArrayList(1); + private boolean excludeDataInTheCurrentTransaction = false; + private ArrayList sortDefinitions = new ArrayList(1); + private Operator defaultOperator = Operator.OR; + + public SearchParameters() + { + super(); + } + + /** + * Set the stores to be supported - currently there can be only one + * + * @param store + */ + public void addStore(StoreRef store) + { + if(stores.size() != 0) + { + throw new IllegalStateException("At the moment, there can only be one store set for the search"); + } + stores.add(store); + } + + /** + * Add paths for attributes in the result set + * + * @param attributePath + */ + public void addAttrbutePath(Path attributePath) + { + attributePaths.add(attributePath); + } + + /** + * Add parameter definitions for the query - used to parameterise the query string + * + * @param queryParameterDefinition + */ + public void addQueryParameterDefinition(QueryParameterDefinition queryParameterDefinition) + { + queryParameterDefinitions.add(queryParameterDefinition); + } + + /** + * If true, any data in the current transaction will be ignored in the search. + * You will not see anything you have added in the current transaction. + * + * @param excludeDataInTheCurrentTransaction + */ + public void excludeDataInTheCurrentTransaction(boolean excludeDataInTheCurrentTransaction) + { + this.excludeDataInTheCurrentTransaction = excludeDataInTheCurrentTransaction; + } + + /** + * Add a sort to the query (for those query languages that do not support it directly) + * + * @param field - this is intially a direct attribute on a node not an attribute on the parent etc + * TODO: It could be a relative path at some time. + * + * + * @param ascending + */ + public void addSort(String field, boolean ascending) + { + addSort(new SortDefinition(SortDefinition.SortType.FIELD, field, ascending)); + } + + public void addSort(SortDefinition sortDefinition) + { + sortDefinitions.add(sortDefinition); + } + + /** + * A helper class for sort definition + * @author andyh + * + * TODO To change the template for this generated type comment go to + * Window - Preferences - Java - Code Style - Code Templates + */ + public static class SortDefinition + { + + public enum SortType {FIELD, DOCUMENT, SCORE}; + + SortType sortType; + String field; + boolean ascending; + + SortDefinition(SortType sortType, String field, boolean ascending) + { + this.sortType = sortType; + this.field = field; + this.ascending = ascending; + } + + public boolean isAscending() + { + return ascending; + } + + public String getField() + { + return field; + } + + public SortType getSortType() + { + return sortType; + } + + } + + public ArrayList getAttributePaths() + { + return attributePaths; + } + + public boolean excludeDataInTheCurrentTransaction() + { + return excludeDataInTheCurrentTransaction; + } + + public ArrayList getQueryParameterDefinitions() + { + return queryParameterDefinitions; + } + + public ArrayList getSortDefinitions() + { + return sortDefinitions; + } + + public ArrayList getStores() + { + return stores; + } + + public void setDefaultOperator(Operator defaultOperator) + { + this.defaultOperator = defaultOperator; + } + + public Operator getDefaultOperator() + { + return defaultOperator; + } +} diff --git a/source/java/org/alfresco/service/cmr/search/SearchService.java b/source/java/org/alfresco/service/cmr/search/SearchService.java new file mode 100644 index 0000000000..231265fee2 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/SearchService.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.XPathException; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; + +/** + * This encapsulates the execution of search against different indexing + * mechanisms. + * + * Canned queries have been translated into the query string by this stage. + * Handling of parameterisation is left to the implementation. + * + * @author Andy hind + * + */ +public interface SearchService +{ + public static final String LANGUAGE_LUCENE = "lucene"; + + public static final String LANGUAGE_XPATH = "xpath"; + + public static final String LANGUAGE_JCR_XPATH = "jcr-xpath"; + + /** + * Search against a store. + * + * @param store - + * the store against which to search + * @param language - + * the query language + * @param query - + * the query string - which may include parameters + * @param attributePaths - + * explicit list of attributes/properties to extract for the + * selected nodes in xpath style syntax + * @param queryParameterDefinition - + * query parameter definitions - the default value is used for + * the value. + * @return Returns the query results + */ + public ResultSet query(StoreRef store, String language, String query, Path[] attributePaths, + QueryParameterDefinition[] queryParameterDefinitions); + + /** + * Search against a store. Pulls back all attributes on each node. Does not + * allow parameterisation. + * + * @param store - + * the store against which to search + * @param language - + * the query language + * @param query - + * the query string - which may include parameters + * @return Returns the query results + */ + public ResultSet query(StoreRef store, String language, String query); + + /** + * Search against a store. + * + * @param store - + * the store against which to search + * @param language - + * the query language + * @param query - + * the query string - which may include parameters + * @param queryParameterDefinition - + * query parameter definitions - the default value is used for + * the value. + * @return Returns the query results + */ + public ResultSet query(StoreRef store, String language, String query, + QueryParameterDefinition[] queryParameterDefintions); + + /** + * Search against a store. + * + * @param store - + * the store against which to search + * @param language - + * the query language + * @param query - + * the query string - which may include parameters + * @param attributePaths - + * explicit list of attributes/properties to extract for the + * selected nodes in xpath style syntax + * @return Returns the query results + */ + public ResultSet query(StoreRef store, String language, String query, Path[] attributePaths); + + /** + * Execute a canned query + * + * @param store - + * the store against which to search + * @param queryId - + * the query identifier + * @param queryParameters - + * parameterisation for the canned query + * @return Returns the query results + */ + public ResultSet query(StoreRef store, QName queryId, QueryParameter[] queryParameters); + + /** + * Search using the given SearchParameters + */ + + public ResultSet query(SearchParameters searchParameters); + + /** + * Select nodes using an xpath expression. + * + * @param contextNodeRef - + * the context node for relative expressions etc + * @param xpath - + * the xpath string to evaluate + * @param parameters - + * parameters to bind in to the xpath expression + * @param namespacePrefixResolver - + * prefix to namespace mappings + * @param followAllParentLinks - + * if false ".." follows only the primary parent links, if true + * it follows all + * @return a list of all the child assoc relationships to the selected nodes + */ + public List selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, + NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + throws InvalidNodeRefException, XPathException; + + /** + * Select nodes using an xpath expression. + * + * @param contextNodeRef - + * the context node for relative expressions etc + * @param xpath - + * the xpath string to evaluate + * @param parameters - + * parameters to bind in to the xpath expression + * @param namespacePrefixResolver - + * prefix to namespace mappings + * @param followAllParentLinks - + * if false ".." follows only the primary parent links, if true + * it follows all + * @param langauage - + * the xpath variant + * @return a list of all the child assoc relationships to the selected nodes + */ + public List selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters, + NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks, String language) + throws InvalidNodeRefException, XPathException; + + /** + * Select properties using an xpath expression + * + * @param contextNodeRef - + * the context node for relative expressions etc + * @param xpath - + * the xpath string to evaluate + * @param parameters - + * parameters to bind in to the xpath expression + * @param namespacePrefixResolver - + * prefix to namespace mappings + * @param followAllParentLinks - + * if false ".." follows only the primary parent links, if true + * it follows all + * @return a list of property values + */ + public List selectProperties(NodeRef contextNodeRef, String xpath, + QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks) throws InvalidNodeRefException, XPathException; + + /** + * Select properties using an xpath expression + * + * @param contextNodeRef - + * the context node for relative expressions etc + * @param xpath - + * the xpath string to evaluate + * @param parameters - + * parameters to bind in to the xpath expression + * @param namespacePrefixResolver - + * prefix to namespace mappings + * @param followAllParentLinks - + * if false ".." follows only the primary parent links, if true + * it follows all + * @param langauage - + * the xpath variant + * @return a list of property values + */ + public List selectProperties(NodeRef contextNodeRef, String xpath, + QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, + boolean followAllParentLinks, String language) throws InvalidNodeRefException, XPathException; + + /** + * Search for string pattern in both the node text (if present) and node + * properties + * + * @param nodeRef + * the node to get + * @param propertyQName + * the name of the property + * @param googleLikePattern + * a Google-like pattern to search for in the property value + * @return Returns true if the pattern could be found - uses the default OR operator + */ + public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern) + throws InvalidNodeRefException; + + /** + * Search for string pattern in both the node text (if present) and node + * properties + * + * @param nodeRef + * the node to get + * @param propertyQName + * the name of the property + * @param googleLikePattern + * a Google-like pattern to search for in the property value + * @return Returns true if the pattern could be found + */ + public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern, SearchParameters.Operator defaultOperator) + throws InvalidNodeRefException; + + /** + * Search for string pattern in both the node text (if present) and node + * properties + * + * @param nodeRef + * the node to get + * @param propertyQName + * the name of the property (mandatory) + * @param sqlLikePattern + * a SQL-like pattern to search for + * @param includeFTS - + * include full text search matches in the like test + * @return Returns true if the pattern could be found + */ + public boolean like(NodeRef nodeRef, QName propertyQName, String sqlLikePattern, boolean includeFTS) + throws InvalidNodeRefException; +} diff --git a/source/java/org/alfresco/service/cmr/search/SearchStatement.java b/source/java/org/alfresco/service/cmr/search/SearchStatement.java new file mode 100644 index 0000000000..cc12714a32 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/search/SearchStatement.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.search; + +/** + * A search string and language. + * + * @author Andy Hind + */ +public class SearchStatement +{ + + private String language; + private String query; + + SearchStatement() + { + super(); + } + + SearchStatement(String language, String query) + { + this.language = language; + this.query = query; + } + + public String getLanguage() + { + return language; + } + + public String getQuery() + { + return query; + } + + public void setLanguage(String language) + { + this.language = language; + } + + public void setQuery(String query) + { + this.query = query; + } + +} diff --git a/source/java/org/alfresco/service/cmr/security/AccessPermission.java b/source/java/org/alfresco/service/cmr/security/AccessPermission.java new file mode 100644 index 0000000000..55434a5438 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/AccessPermission.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + + +/** + * The interface used to support reporting back if permissions are allowed or + * denied. + * + * @author Andy Hind + */ +public interface AccessPermission +{ + /** + * The permission. + * + * @return + */ + public String getPermission(); + + /** + * Get the Access enumeration value + * + * @return + */ + public AccessStatus getAccessStatus(); + + + /** + * Get the authority to which this permission applies. + * + * @return + */ + public String getAuthority(); + + + /** + * Get the type of authority to which this permission applies. + * + * @return + */ + public AuthorityType getAuthorityType(); +} diff --git a/source/java/org/alfresco/service/cmr/security/AccessStatus.java b/source/java/org/alfresco/service/cmr/security/AccessStatus.java new file mode 100644 index 0000000000..acb2fd5185 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/AccessStatus.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +/** + * Enumeration used to indicate access status. + * + * @author Andy Hind + */ +public enum AccessStatus +{ + DENIED, ALLOWED +} diff --git a/source/java/org/alfresco/service/cmr/security/AuthenticationService.java b/source/java/org/alfresco/service/cmr/security/AuthenticationService.java new file mode 100644 index 0000000000..b37e21202b --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/AuthenticationService.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +import org.alfresco.repo.security.authentication.AuthenticationException; + +/** + * The authentication service defines the API for managing authentication information + * against a user id. + * + * @author Andy Hind + * + */ +public interface AuthenticationService +{ + /** + * Create an authentication for the given user. + * + * @param userName + * @param password + * @throws AuthenticationException + */ + public void createAuthentication(String userName, char[] password) throws AuthenticationException; + + /** + * Update the login information for the user (typically called by the user) + * + * @param userName + * @param oldPassword + * @param newPassword + * @throws AuthenticationException + */ + public void updateAuthentication(String userName, char[] oldPassword, char[] newPassword) throws AuthenticationException; + + /** + * Set the login information for a user (typically called by an admin user) + * + * @param userName + * @param newPassword + * @throws AuthenticationException + */ + public void setAuthentication(String userName, char[] newPassword) throws AuthenticationException; + + + /** + * Delete an authentication entry + * + * @param userName + * @throws AuthenticationException + */ + public void deleteAuthentication(String userName) throws AuthenticationException; + + /** + * Enable or disable an authentication entry + * + * @param userName + * @param enabled + */ + public void setAuthenticationEnabled(String userName, boolean enabled) throws AuthenticationException; + + /** + * Is an authentication enabled or disabled? + * + * @param userName + * @return + */ + public boolean getAuthenticationEnabled(String userName) throws AuthenticationException; + + /** + * Carry out an authentication attempt. If successful the user is set to the current user. + * The current user is a part of the thread context. + * + * @param userName + * @param password + * @throws AuthenticationException + */ + public void authenticate(String userName, char[] password) throws AuthenticationException; + + /** + * Get the name of the currently authenticated user. + * + * @return + * @throws AuthenticationException + */ + public String getCurrentUserName() throws AuthenticationException; + + /** + * Invalidate any tickets held by the user. + * + * @param userName + * @throws AuthenticationException + */ + public void invalidateUserSession(String userName) throws AuthenticationException; + + /** + * Invalidate a single ticket by ID + * + * @param ticket + * @throws AuthenticationException + */ + public void invalidateTicket(String ticket) throws AuthenticationException; + + /** + * Validate a ticket. Set the current user name accordingly. + * + * @param ticket + * @throws AuthenticationException + */ + public void validate(String ticket) throws AuthenticationException; + + /** + * Get the current ticket as a string + * @return + */ + public String getCurrentTicket(); + + /** + * Remove the current security information + * + */ + public void clearCurrentSecurityContext(); + + /** + * Is the current user the system user? + * + * @return + */ + + public boolean isCurrentUserTheSystemUser(); + +} + diff --git a/source/java/org/alfresco/service/cmr/security/AuthorityService.java b/source/java/org/alfresco/service/cmr/security/AuthorityService.java new file mode 100644 index 0000000000..c23348d855 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/AuthorityService.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +import java.util.Set; + +/** + * The service that encapsulates authorities granted to users. + * + * This service will refuse to create any user authorities. These should be + * managed using the AuthenticationService and PersonServce. Methods that try to + * change alter users will throw an exception. + * + * A string key is used to identify the authority. These follow the contract + * defined in AuthorityType. If there are entities linked to these authorities + * this key should be used to find them, as userName is used link user and + * person. + * + * @author Andy Hind + */ +public interface AuthorityService +{ + /** + * Check of the current user has admin authority. + * + * There is no contract for who should have this authority, only that it can + * be tested here. It could be determined by group membership, role, + * authentication mechanism, ... + * + * @return true if the currently authenticated user has the admin authority + */ + public boolean hasAdminAuthority(); + + /** + * Get the authorities for the current user + * + * @return + */ + public Set getAuthorities(); + + /** + * Get all authorities by type. + * + * @param type - + * the type of authorities. + * @return + */ + public Set getAllAuthorities(AuthorityType type); + + /** + * Get all root authorities by type. Root authorities are ones that were + * created without an authority as the parent authority; + * + * @param type - + * the type of the authority + * @return + */ + + public Set getAllRootAuthorities(AuthorityType type); + + /** + * Create an authority. If the parent is null thisw method creates a root + * authority. + * + * @param type - + * the type of the authority + * @param parentName - + * the name of the parent authority. If this is null then a root + * authority is created. + * @param shortName - + * the short name of the authority to create + * + * @return the name of the authority (this will be the prefix, if any + * associated with the type appended with the short name) + */ + public String createAuthority(AuthorityType type, String parentName, String shortName); + + /** + * Set an authority to include another authority. For example, adding a + * group to a group or adding a user to a group. + * + * @param parentName - + * the string identifier for the parent. + * @param childName - + * the string identifier for the child. + */ + public void addAuthority(String parentName, String childName); + + /** + * Remove an authority as a member of another authority. The child authority + * will still exist. If the child authority was not created as a root + * authority and you remove its creation link, it will be moved to a root + * authority. If you want rid of it, use delete. + * + * @param parentName - + * the string identifier for the parent. + * @param childName - + * the string identifier for the child. + */ + public void removeAuthority(String parentName, String childName); + + /** + * Delete an authority and all its relationships. + * + * @param name + */ + public void deleteAuthority(String name); + + /** + * Get all the authorities that are contained by the given authority. + * + * For a group you could get all the authorities it contains, just the users + * it contains or just the other groups it includes. + * + * @param type - + * if not null, limit to the type of authority specified + * @param name - + * the name of the containing authority + * @param immediate - + * if true, limit the depth to just immediate child, if false + * find authorities at any depth + * @return + */ + public Set getContainedAuthorities(AuthorityType type, String name, boolean immediate); + + /** + * Get the authorities that contain the given authority + * + * For example, this can be used find out all the authorities that contain a + * user. + * + * @param type - + * if not null, limit to the type of authority specified + * @param name - + * the name of the authority for which the containing authorities + * are required. + * @param immediate - + * limit to immediate parents or any ancestor. + * @return + */ + public Set getContainingAuthorities(AuthorityType type, String name, boolean immediate); + + /** + * Extract the short name of an authority from its full identifier. + * + * @param name + * @return + */ + public String getShortName(String name); + + /** + * Create the full identifier for an authority given its short name and + * type. + * + * @param type + * @param shortName + * @return + */ + public String getName(AuthorityType type, String shortName); + +} diff --git a/source/java/org/alfresco/service/cmr/security/AuthorityType.java b/source/java/org/alfresco/service/cmr/security/AuthorityType.java new file mode 100644 index 0000000000..b7ad08cedc --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/AuthorityType.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +/** + * The types of authority that are available. + *

    + *

    + * Available types are: + *

      + *
    1. USER - an authority that identifies a user + *
    2. GROUP - an authority that identifies a group + *
    3. OWNER - the special authority that applies to the owner of a node + *
    4. EVERYONE - the special authority that is interpreted as everyone + *
    5. GUEST - the special authority that applies to a GUEST (An unknown, + * unauthenticated user) + *
    + * + * @author Andy Hind + */ +public enum AuthorityType +{ + ADMIN + { + public boolean isFixedString() + { + return true; + } + + public String getFixedString() + { + return PermissionService.ADMINISTRATOR_AUTHORITY; + } + + public boolean isPrefixed() + { + return false; + } + + public String getPrefixString() + { + return ""; + } + }, + + EVERYONE + { + public boolean isFixedString() + { + return true; + } + + public String getFixedString() + { + return PermissionService.ALL_AUTHORITIES; + } + + public boolean isPrefixed() + { + return false; + } + + public String getPrefixString() + { + return ""; + } + }, + OWNER + { + public boolean isFixedString() + { + return true; + } + + public String getFixedString() + { + return PermissionService.OWNER_AUTHORITY; + } + + public boolean isPrefixed() + { + return false; + } + + public String getPrefixString() + { + return ""; + } + }, + GUEST + { + public boolean isFixedString() + { + return true; + } + + public String getFixedString() + { + return PermissionService.GUEST; + } + + public boolean isPrefixed() + { + return false; + } + + public String getPrefixString() + { + return ""; + } + }, + GROUP + { + public boolean isFixedString() + { + return false; + } + + public String getFixedString() + { + return ""; + } + + public boolean isPrefixed() + { + return true; + } + + public String getPrefixString() + { + return PermissionService.GROUP_PREFIX; + } + }, + ROLE + { + + public boolean isFixedString() + { + return false; + } + + public String getFixedString() + { + return ""; + } + + public boolean isPrefixed() + { + return true; + } + + public String getPrefixString() + { + return PermissionService.ROLE_PREFIX; + } + }, + USER + { + public boolean isFixedString() + { + return false; + } + + public String getFixedString() + { + return ""; + } + + public boolean isPrefixed() + { + return false; + } + + public String getPrefixString() + { + return ""; + } + }; + + public abstract boolean isFixedString(); + + public abstract String getFixedString(); + + public abstract boolean isPrefixed(); + + public abstract String getPrefixString(); + + public boolean equals(String authority) + { + return equals(getAuthorityType(authority)); + } + + public static AuthorityType getAuthorityType(String authority) + { + AuthorityType authorityType; + if (authority.equals(PermissionService.ADMINISTRATOR_AUTHORITY)) + { + authorityType = AuthorityType.ADMIN; + } + if (authority.equals(PermissionService.ALL_AUTHORITIES)) + { + authorityType = AuthorityType.EVERYONE; + } + else if (authority.equals(PermissionService.OWNER_AUTHORITY)) + { + authorityType = AuthorityType.OWNER; + } + else if (authority.equals(PermissionService.GUEST)) + { + authorityType = AuthorityType.GUEST; + } + else if (authority.startsWith(PermissionService.GROUP_PREFIX)) + { + authorityType = AuthorityType.GROUP; + } + else if (authority.startsWith(PermissionService.ROLE_PREFIX)) + { + authorityType = AuthorityType.ROLE; + } + else + { + authorityType = AuthorityType.USER; + } + return authorityType; + } +} diff --git a/source/java/org/alfresco/service/cmr/security/OwnableService.java b/source/java/org/alfresco/service/cmr/security/OwnableService.java new file mode 100644 index 0000000000..d0f7af05aa --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/OwnableService.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Service support around managing ownership. + * + * @author Andy Hind + */ +public interface OwnableService +{ + /** + * Get the username of the owner of the given object. + * + * @param nodeRef + * @return the username or null if the object has no owner + */ + public String getOwner(NodeRef nodeRef); + + /** + * Set the owner of the object. + * + * @param nodeRef + * @param userName + */ + public void setOwner(NodeRef nodeRef, String userName); + + /** + * Set the owner of the object to be the current user. + * + * @param nodeRef + */ + public void takeOwnership(NodeRef nodeRef); + + /** + * Does the given node have an owner? + * + * @param nodeRef + * @return + */ + public boolean hasOwner(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/security/PermissionService.java b/source/java/org/alfresco/service/cmr/security/PermissionService.java new file mode 100644 index 0000000000..effa503a3d --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/PermissionService.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * The public API for a permission service + * + * The implementation may be changed in the application configuration + * + * @author Andy Hind + */ +public interface PermissionService +{ + public static final String ROLE_PREFIX = "ROLE_"; + + public static final String GROUP_PREFIX = "GROUP_"; + + + + public static final String ALL_AUTHORITIES = "GROUP_EVERYONE"; + + public static final String OWNER_AUTHORITY = "ROLE_OWNER"; + + public static final String LOCK_OWNER_AUTHORITY = "ROLE_LOCK_OWNER"; + + public static final String ADMINISTRATOR_AUTHORITY = "ROLE_ADMINISTRATOR"; + + + + + public static final String ALL_PERMISSIONS = "All"; + + public static final String FULL_CONTROL = "FullControl"; + + public static final String READ = "Read"; + + public static final String WRITE = "Write"; + + public static final String DELETE = "Delete"; + + public static final String ADD_CHILDREN = "AddChildren"; + + public static final String READ_PROPERTIES = "ReadProperties"; + + public static final String READ_CHILDREN = "ReadChildren"; + + public static final String WRITE_PROPERTIES = "WriteProperties"; + + public static final String DELETE_NODE = "DeleteNode"; + + public static final String DELETE_CHILDREN = "DeleteChildren"; + + public static final String CREATE_CHILDREN = "CreateChildren"; + + public static final String LINK_CHILDREN = "LinkChildren"; + + public static final String DELETE_ASSOCIATIONS = "DeleteAssociations"; + + public static final String READ_ASSOCIATIONS = "ReadAssociations"; + + public static final String CREATE_ASSOCIATIONS = "CreateAssociations"; + + public static final String READ_PERMISSIONS = "ReadPermissions"; + + public static final String CHANGE_PERMISSIONS = "ChangePermissions"; + + public static final String EXECUTE = "Execute"; + + public static final String READ_CONTENT = "ReadContent"; + + public static final String WRITE_CONTENT = "WriteContent"; + + public static final String EXECUTE_CONTENT = "ExecuteContent"; + + public static final String TAKE_OWNERSHIP = "TakeOwnership"; + + public static final String SET_OWNER = "SetOwner"; + + public static final String COORDINATOR = "Coordinator"; + + public static final String CONTRIBUTOR = "Contributor"; + + public static final String EDITOR = "Editor"; + + public static final String GUEST = "Guest"; + + public static final String LOCK = "Lock"; + + public static final String UNLOCK = "Unlock"; + + public static final String CHECK_OUT = "CheckOut"; + + public static final String CHECK_IN = "CheckIn"; + + public static final String CANCEL_CHECK_OUT = "CancelCheckOut"; + + /** + * Get the Owner Authority + * + * @return the owner authority + */ + public String getOwnerAuthority(); + + /** + * Get the All Authorities + * + * @return the All authorities + */ + public String getAllAuthorities(); + + /** + * Get the All Permission + * + * @return the All permission + */ + public String getAllPermission(); + + /** + * Get all the AccessPermissions that are granted/denied to the current + * authentication for the given node + * + * @param nodeRef - + * the reference to the node + * @return the set of allowed permissions + */ + public Set getPermissions(NodeRef nodeRef); + + /** + * Get all the AccessPermissions that are set for anyone for the + * given node + * + * @param nodeRef - + * the reference to the node + * @return the set of allowed permissions + */ + public Set getAllSetPermissions(NodeRef nodeRef); + + /** + * Get the permissions that can be set for a given node + * + * @param nodeRef + * @return + */ + public Set getSettablePermissions(NodeRef nodeRef); + + /** + * Get the permissions that can be set for a given type + * + * @param nodeRef + * @return + */ + public Set getSettablePermissions(QName type); + + /** + * Check that the given authentication has a particular permission for the + * given node. (The default behaviour is to inherit permissions) + * + * @param nodeRef + * @param perm + * @return + */ + public AccessStatus hasPermission(NodeRef nodeRef, String perm); + + /** + * Delete all the permission assigned to the node + * + * @param nodeRef + */ + public void deletePermissions(NodeRef nodeRef); + + /** + * Delete all permission for the given authority. + * + * @param nodeRef + * @param authority + */ + public void clearPermission(NodeRef nodeRef, String authority); + + /** + * Find and delete a permission by node, authentication and permission + * definition. + * + * @param nodeRef + * @param authority + * @param perm + */ + public void deletePermission(NodeRef nodeRef, String authority, String perm, boolean allow); + + /** + * Set a specific permission on a node. + * + * @param nodeRef + * @param authority + * @param perm + * @param allow + */ + public void setPermission(NodeRef nodeRef, String authority, String perm, boolean allow); + + /** + * Set the global inheritance behaviour for permissions on a node. + * + * @param nodeRef + * @param inheritParentPermissions + */ + public void setInheritParentPermissions(NodeRef nodeRef, boolean inheritParentPermissions); + + /** + * Return the global inheritance behaviour for permissions on a node. + * + * @param nodeRef + * @return inheritParentPermissions + */ + public boolean getInheritParentPermissions(NodeRef nodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/security/PersonService.java b/source/java/org/alfresco/service/cmr/security/PersonService.java new file mode 100644 index 0000000000..a029fad416 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/security/PersonService.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.security; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * This service encapsulates the management of people and groups. + *

    + *

    + * People and groups may be managed entirely in the repository or entirely in + * some other implementation such as LDAP or via NTLM. Some properties may in + * the repository and some in another store. Individual properties may or may + * not be mutable. + *

    + * + * @author Andy Hind + */ +public interface PersonService +{ + /** + * Get a person by userName. The person is store in the repository. The + * person may be created as a side effect of this call. + * + * @param userName - the userName key to find the person + * @return + */ + public NodeRef getPerson(String userName); + + /** + * Check if a person exists. + * + * @param userName + * @return + */ + public boolean personExists(String userName); + + /** + * Does this service create people on demand if they are missing. If this is + * true, a call to getPerson() will create a person if they are missing. + * + * @return true if people are created on demand and false otherwise. + */ + public boolean createMissingPeople(); + + /** + * Set if missing people should be created. + * + * @param createMissing + */ + public void setCreateMissingPeople(boolean createMissing); + + /** + * Get the list of properties that are mutable. Some service may only allow + * a limited list of properties to be changed. This may be those persisted + * in the repository or those that can be changed in some other + * implementation such as LDAP. + * + * @return A set of QNames that identify properties that can be changed + */ + public Set getMutableProperties(); + + /** + * Set the properties on a person - some of these may be persisted in + * different locations. + * + * @param userName - the user for which the properties should be set. + * @param properties - the map of properties to set (as the NodeService) + */ + public void setPersonProperties(String userName, Map properties); + + /** + * Can this service create, delete and update person information? + * + * @return true if this service allows mutation to people. + */ + public boolean isMutable(); + + /** + * Create a new person with the given properties. + * The userName is one of the properties. + * Users with duplicate userNames are not allowed. + * + * @param properties + * @return + */ + public NodeRef createPerson(Map properties); + + /** + * Delete the person identified by the given user name. + * + * @param userName + */ + public void deletePerson(String userName); + + /** + * Get all the people we know about. + * + * @return a set of people in no specific order. + */ + public Set getAllPeople(); + + /** + * Return the container that stores people. + * + * @return + */ + public NodeRef getPeopleContainer(); + + /** + * Are user names case sensitive? + * + * @return + */ + public boolean getUserNamesAreCaseSensitive(); +} diff --git a/source/java/org/alfresco/service/cmr/version/ReservedVersionNameException.java b/source/java/org/alfresco/service/cmr/version/ReservedVersionNameException.java new file mode 100644 index 0000000000..b5935e4345 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/ReservedVersionNameException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +import java.text.MessageFormat; + +/** + * @author Roy Wetherall + */ +public class ReservedVersionNameException extends RuntimeException +{ + /** + * Serial verison UID + */ + private static final long serialVersionUID = 3690478030330015795L; + + /** + * Error message + */ + private static final String MESSAGE = "The version property name {0} clashes with a reserved verison property name."; + + /** + * Constructor + * + * @param propertyName the name of the property that clashes with + * a reserved property name + */ + public ReservedVersionNameException(String propertyName) + { + super(MessageFormat.format(MESSAGE, new Object[]{propertyName})); + } +} diff --git a/source/java/org/alfresco/service/cmr/version/Version.java b/source/java/org/alfresco/service/cmr/version/Version.java new file mode 100644 index 0000000000..370e7992d3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/Version.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; + + +/** + * Version interface. + * + * Allows access to version property values and frozen state node references. + * The version history tree can also be navigated. + * + * @author Roy Wetherall + */ +public interface Version extends Serializable +{ + /** + * Names of the system version properties + */ + public static final String PROP_DESCRIPTION = "description"; + + /** + * Helper method to get the created date from the version property data. + * + * @return the date the version was created + */ + public Date getCreatedDate(); + + /** + * Helper method to get the creator of the version. + * + * @return the creator of the version + */ + public String getCreator(); + + /** + * Helper method to get the version label from the version property data. + * + * @return the version label + */ + public String getVersionLabel(); + + /** + * Helper method to get the version type. + * + * @return the value of the version type as an enum value + */ + public VersionType getVersionType(); + + /** + * Helper method to get the version description. + * + * @return the version description + */ + public String getDescription(); + + /** + * Get the map containing the version property values + * + * @return the map containing the version properties + */ + public Map getVersionProperties(); + + /** + * Gets the value of a named version property. + * + * @param name the name of the property + * @return the value of the property + * + */ + public Serializable getVersionProperty(String name); + + /** + * Gets a reference to the node that this version was created from. + *

    + * Note that this reference will be to the current state of the versioned + * node which may now correspond to a later version. + * + * @return a node reference + */ + public NodeRef getVersionedNodeRef(); + + /** + * Gets the reference to the node that contains the frozen state of the + * version. + * + * @return a node reference + */ + public NodeRef getFrozenStateNodeRef(); +} diff --git a/source/java/org/alfresco/service/cmr/version/VersionDoesNotExistException.java b/source/java/org/alfresco/service/cmr/version/VersionDoesNotExistException.java new file mode 100644 index 0000000000..b5cfa31c21 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/VersionDoesNotExistException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +import java.text.MessageFormat; + + +/** + * Version does not exist exception class. + * + * @author Roy Wetherall + */ +public class VersionDoesNotExistException extends VersionServiceException +{ + private static final long serialVersionUID = 3258133548417233463L; + private static final String ERROR_MESSAGE = "The version with label {0} does not exisit in the version store."; + + /** + * Constructor + */ + public VersionDoesNotExistException(String versionLabel) + { + super(MessageFormat.format(ERROR_MESSAGE, new Object[]{versionLabel})); + } +} diff --git a/source/java/org/alfresco/service/cmr/version/VersionHistory.java b/source/java/org/alfresco/service/cmr/version/VersionHistory.java new file mode 100644 index 0000000000..8b2bfe0f2c --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/VersionHistory.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +import java.io.Serializable; +import java.util.Collection; + + + +/** + * Version history interface. + * + * Collects the versions that make-up a version history. + * + * @author Roy Wetherall + */ +public interface VersionHistory extends Serializable +{ + /** + * Gets the root (or initial) version of the version history. + * + * @return the root version + */ + public Version getRootVersion(); + + /** + * Gets a collection containing all the versions within the + * version history. + *

    + * The order of the versions is not guarenteed. + * + * @return collection containing all the versions + */ + public Collection getAllVersions(); + + /** + * Gets the predecessor of a specified version + * + * @param version the version object + * @return the predeceeding version, null if root version + */ + public Version getPredecessor(Version version); + + /** + * Gets the succeeding versions of a specified version. + * + * @param version the version object + * @return a collection containing the succeeding version, empty is none + */ + public Collection getSuccessors(Version version); + + /** + * Gets a version with a specified version label. The version label is guarenteed + * unique within the version history. + * + * @param versionLabel the version label + * @return the version object + * @throws VersionDoesNotExistException indicates requested version does not exisit + */ + public Version getVersion(String versionLabel); + +} diff --git a/source/java/org/alfresco/service/cmr/version/VersionService.java b/source/java/org/alfresco/service/cmr/version/VersionService.java new file mode 100644 index 0000000000..c0b6481e40 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/VersionService.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Map; + +import org.alfresco.service.cmr.repository.AspectMissingException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + +/** + * Interface for public and internal version operations. + * + * @author Roy Wetherall + */ +public interface VersionService +{ + /** + * The version store protocol label, used in store references + */ + public static final String VERSION_STORE_PROTOCOL = "versionStore"; + + /** + * Gets the reference to the version store + * + * @return reference to the version store + */ + public StoreRef getVersionStoreReference(); + + /** + * Creates a new version based on the referenced node. + *

    + * If the node has not previously been versioned then a version history and + * initial version will be created. + *

    + * If the node referenced does not or can not have the version aspect + * applied to it then an exception will be raised. + *

    + * The version properties are sotred as version meta-data against the newly + * created version. + * + * @param nodeRef a node reference + * @param versionProperties the version properties that are stored with the newly created + * version + * @return the created version object + * @throws ReservedVersionNameException + * thrown if a reserved property name is used int he version properties + * provided + * @throws AspectMissingException + * thrown if the version aspect is missing + */ + public Version createVersion( + NodeRef nodeRef, + Map versionProperties) + throws ReservedVersionNameException, AspectMissingException; + + /** + * Creates a new version based on the referenced node. + *

    + * If the node has not previously been versioned then a version history and + * initial version will be created. + *

    + * If the node referenced does not or can not have the version aspect + * applied to it then an exception will be raised. + *

    + * The version properties are sotred as version meta-data against the newly + * created version. + * + * @param nodeRef a node reference + * @param versionProperties the version properties that are stored with the newly created + * version + * @param versionChildren if true then the children of the referenced node are also + * versioned, false otherwise + * @return the created version object(s) + * @throws ReservedVersionNameException + * thrown if a reserved property name is used int he version properties + * provided + * @throws AspectMissingException + * thrown if the version aspect is missing + */ + public Collection createVersion( + NodeRef nodeRef, + Map versionProperties, + boolean versionChildren) + throws ReservedVersionNameException, AspectMissingException; + + /** + * Creates new versions based on the list of node references provided. + * + * @param nodeRefs a list of node references + * @param versionProperties version property values + * @return a collection of newly created versions + * @throws ReservedVersionNameException + * thrown if a reserved property name is used int he version properties + * provided + * @throws AspectMissingException + * thrown if the version aspect is missing + */ + public Collection createVersion( + Collection nodeRefs, + Map versionProperties) + throws ReservedVersionNameException, AspectMissingException; + + /** + * Gets the version history information for a node. + *

    + * If the node has not been versioned then null is returned. + *

    + * If the node referenced does not or can not have the version aspect + * applied to it then an exception will be raised. + * + * @param nodeRef a node reference + * @return the version history information + * @throws AspectMissingException + * thrown if the version aspect is missing + */ + public VersionHistory getVersionHistory(NodeRef nodeRef) + throws AspectMissingException; + + /** + * Gets the version object for the current version of the node reference + * passed. + *

    + * Returns null if the node is not versionable or has not been versioned. + * @param nodeRef the node reference + * @return the version object for the current version + */ + public Version getCurrentVersion(NodeRef nodeRef); + + /** + * The node reference will be reverted to the current version. + *

    + * A deep revert will be performed. + * + * @see VersionService#revert(NodeRef, Version, boolean) + * + * @param nodeRef the node reference + */ + public void revert(NodeRef nodeRef); + + /** + * The node reference will be reverted to the current version. + * + * @see VersionService#revert(NodeRef, Version, boolean) + * + * @param nodeRef the node reference + * @param deep true if a deep revert is to be performed, flase otherwise + */ + public void revert(NodeRef nodeRef, boolean deep); + + /** + * A deep revert will take place by default. + * + * @see VersionService#revert(NodeRef, Version, boolean) + * + * @param nodeRef the node reference + * @param version the version to revert to + */ + public void revert(NodeRef nodeRef, Version version); + + /** + * Revert the state of the node to the specified version. + *

    + * Any changes made to the node will be lost and the state of the node will reflect + * that of the version specified. + *

    + * The version label property on the node reference will remain unchanged. + *

    + * If the node is further versioned then the new version will be created at the head of + * the version history graph. A branch will not be created. + *

    + * If a deep revert is to be performed then any child nodes that are no longer present will + * be deep restored (if appropriate) otherwise child associations to deleted, versioned nodes + * will not be restored. + * + * @param nodeRef the node reference + * @param version the version to revert to + * @param deep true is a deep revert is to be performed, false otherwise. + */ + public void revert(NodeRef nodeRef, Version version, boolean deep); + + /** + * By default a deep restore is performed. + * + * @see org.alfresco.service.cmr.version.VersionService#restore(NodeRef, NodeRef, QName, QName, boolean) + * + * @param nodeRef the node reference to a node that no longer exists in the store + * @param parentNodeRef the new parent of the restored node + * @param assocTypeQName the assoc type qname + * @param assocQName the assoc qname + * @return the newly restored node reference + */ + public NodeRef restore( + NodeRef nodeRef, + NodeRef parentNodeRef, + QName assocTypeQName, + QName assocQName); + + /** + * Restores a node not currenlty present in the store, but that has a version + * history. + *

    + * The restored node will be at the head (most resent version). + *

    + * Resoration will fail if there is no version history for the specified node id in + * the specified store. + *

    + * If the node already exists in the store then an exception will be raised. + *

    + * Once the node is restored it is reverted to the head version in the appropriate + * version history tree. If deep is set to true then this will be a deep revert, false + * otherwise. + * + * @param nodeRef the node reference to a node that no longer exists in + * the store + * @param parentNodeRef the new parent of the resotred node + * @param assocTypeQName the assoc type qname + * @param assocQName the assoc qname + * @param deep true is a deep revert shoudl be performed once the node has been + * restored, false otherwise + * @return the newly restored node reference + */ + public NodeRef restore( + NodeRef nodeRef, + NodeRef parentNodeRef, + QName assocTypeQName, + QName assocQName, + boolean deep); + + /** + * Delete the version history associated with a node reference. + *

    + * This operation is perminant, all versions in the version history are + * deleted and cannot be retrieved. + *

    + * The current version label for the node reference is reset and any subsequent versions + * of the node will result in a new version history being created. + * + * @param nodeRef the node reference + * @throws AspectMissingException thrown if the version aspect is missing + */ + public void deleteVersionHistory(NodeRef nodeRef) + throws AspectMissingException; +} diff --git a/source/java/org/alfresco/service/cmr/version/VersionServiceException.java b/source/java/org/alfresco/service/cmr/version/VersionServiceException.java new file mode 100644 index 0000000000..4ba82de7db --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/VersionServiceException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Version service exception class. + * + * @author Roy Wetherall + */ +public class VersionServiceException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 3544671772030349881L; + + public VersionServiceException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + public VersionServiceException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } + + public VersionServiceException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + public VersionServiceException(String msgId) + { + super(msgId); + } +} diff --git a/source/java/org/alfresco/service/cmr/version/VersionType.java b/source/java/org/alfresco/service/cmr/version/VersionType.java new file mode 100644 index 0000000000..0297131f3f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/version/VersionType.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.version; + +/** + * Version type enum. + *

    + * Commonly used in the version meta data to indicate whether the version is a major ro minor + * change. + * + * @author Roy Wetherall + */ +public enum VersionType {MAJOR, MINOR} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/view/ExportPackageHandler.java b/source/java/org/alfresco/service/cmr/view/ExportPackageHandler.java new file mode 100644 index 0000000000..0383e097d5 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ExportPackageHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import java.io.InputStream; +import java.io.OutputStream; + +import org.alfresco.service.cmr.repository.ContentData; + + +/** + * Contract for a custom content property exporter. + * + * @author David Caruana + * + */ +public interface ExportPackageHandler +{ + /** + * Start the Export + */ + public void startExport(); + + /** + * Create a stream for accepting the package data + * + * @return the output stream + */ + public OutputStream createDataStream(); + + + /** + * Call-back for handling the export of content stream. + * + * @param content content to export + * @param contentData content descriptor + * @return the URL to the location of the exported content + */ + public ContentData exportContent(InputStream content, ContentData contentData); + + /** + * End the Export + */ + public void endExport(); + +} diff --git a/source/java/org/alfresco/service/cmr/view/Exporter.java b/source/java/org/alfresco/service/cmr/view/Exporter.java new file mode 100644 index 0000000000..8aedd7c22d --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/Exporter.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import java.io.InputStream; +import java.util.Collection; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Contract for an exporter. An exporter is responsible for actually exporting + * the content of the Repository to a destination point e.g. file system. + * + * @author David Caruana + */ +public interface Exporter +{ + /** + * Start of Export + */ + public void start(ExporterContext context); + + /** + * Start export of namespace + * + * @param prefix namespace prefix + * @param uri namespace uri + */ + public void startNamespace(String prefix, String uri); + + /** + * End export of namespace + * + * @param prefix namespace prefix + */ + public void endNamespace(String prefix); + + /** + * Start export of node + * + * @param nodeRef the node reference + */ + public void startNode(NodeRef nodeRef); + + /** + * End export of node + * + * @param nodeRef the node reference + */ + public void endNode(NodeRef nodeRef); + + /** + * Start export of aspects + * + * @param nodeRef + */ + public void startAspects(NodeRef nodeRef); + + /** + * Start export of aspect + * + * @param nodeRef the node reference + * @param aspect the aspect + */ + public void startAspect(NodeRef nodeRef, QName aspect); + + /** + * End export of aspect + * + * @param nodeRef the node reference + * @param aspect the aspect + */ + public void endAspect(NodeRef nodeRef, QName aspect); + + /** + * End export of aspects + * + * @param nodeRef + */ + public void endAspects(NodeRef nodeRef); + + /** + * Start export of properties + * + * @param nodeRef the node reference + */ + public void startProperties(NodeRef nodeRef); + + /** + * Start export of property + * + * @param nodeRef the node reference + * @param property the property name + */ + public void startProperty(NodeRef nodeRef, QName property); + + /** + * End export of property + * + * @param nodeRef the node reference + * @param property the property name + */ + public void endProperty(NodeRef nodeRef, QName property); + + /** + * End export of properties + * + * @param nodeRef the node reference + */ + public void endProperties(NodeRef nodeRef); + + /** + * Export single valued property + * + * @param nodeRef the node reference + * @param property the property name + * @param value the value + */ + public void value(NodeRef nodeRef, QName property, Object value); + + /** + * Export multi valued property + * + * @param nodeRef the node reference + * @param property the property name + * @param value the value + */ + public void value(NodeRef nodeRef, QName property, Collection values); + + /** + * Export content stream + * + * @param nodeRef the node reference + * @param property the property name + * @param content the content stream + * @param contentData content descriptor + */ + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData); + + /** + * Start export of associations + * + * @param nodeRef + */ + public void startAssocs(NodeRef nodeRef); + + /** + * Start export of association + * + * @param nodeRef the node reference + * @param assoc the association name + */ + public void startAssoc(NodeRef nodeRef, QName assoc); + + /** + * End export of association + * + * @param nodeRef the node reference + * @param assoc the association name + */ + public void endAssoc(NodeRef nodeRef, QName assoc); + + /** + * End export of associations + * + * @param nodeRef + */ + public void endAssocs(NodeRef nodeRef); + + /** + * Export warning + * + * @param warning the warning message + */ + public void warning(String warning); + + /** + * End export + */ + public void end(); + +} diff --git a/source/java/org/alfresco/service/cmr/view/ExporterContext.java b/source/java/org/alfresco/service/cmr/view/ExporterContext.java new file mode 100644 index 0000000000..6c6564b51f --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ExporterContext.java @@ -0,0 +1,18 @@ +package org.alfresco.service.cmr.view; + +import java.util.Date; + +import org.alfresco.service.cmr.repository.NodeRef; + +public interface ExporterContext +{ + + public String getExportedBy(); + + public Date getExportedDate(); + + public String getExporterVersion(); + + public NodeRef getExportOf(); + +} diff --git a/source/java/org/alfresco/service/cmr/view/ExporterCrawlerParameters.java b/source/java/org/alfresco/service/cmr/view/ExporterCrawlerParameters.java new file mode 100644 index 0000000000..b3cd768039 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ExporterCrawlerParameters.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import org.alfresco.service.namespace.NamespaceService; + + +/** + * Exporter Crawler Configuration. + * + * This class is used to specify which Repository items are exported. + * + * @author David Caruana + */ +public class ExporterCrawlerParameters +{ + + private Location exportFrom = null; + private boolean crawlSelf = false; + private boolean crawlChildNodes = true; + private boolean crawlContent = true; + private boolean crawlNullProperties = true; + private String[] excludeNamespaceURIs = new String[] { NamespaceService.REPOSITORY_VIEW_1_0_URI }; + + + /** + * Crawl and export child nodes + * + * @return true => crawl child nodes + */ + public boolean isCrawlChildNodes() + { + return crawlChildNodes; + } + + /** + * Sets whether to crawl child nodes + * + * @param crawlChildNodes + */ + public void setCrawlChildNodes(boolean crawlChildNodes) + { + this.crawlChildNodes = crawlChildNodes; + } + + /** + * Crawl and export content properties + * + * @return true => crawl content + */ + public boolean isCrawlContent() + { + return crawlContent; + } + + /** + * Sets whether to crawl content + * + * @param crawlContent + */ + public void setCrawlContent(boolean crawlContent) + { + this.crawlContent = crawlContent; + } + + /** + * Crawl and export node at export path + * + * @return true => crawl node at export path + */ + public boolean isCrawlSelf() + { + return crawlSelf; + } + + /** + * Sets whether to crawl and export node at export path + * + * @param crawlSelf + */ + public void setCrawlSelf(boolean crawlSelf) + { + this.crawlSelf = crawlSelf; + } + + /** + * Crawl and export null properties + * + * @return true => export null properties + */ + public boolean isCrawlNullProperties() + { + return crawlNullProperties; + } + + /** + * Sets whether to crawl null properties + * + * @param crawlNullProperties + */ + public void setCrawlNullProperties(boolean crawlNullProperties) + { + this.crawlNullProperties = crawlNullProperties; + } + + /** + * Gets the list of namespace URIs to exlude from the Export + * + * @return the list of namespace URIs + */ + public String[] getExcludeNamespaceURIs() + { + return excludeNamespaceURIs; + } + + /** + * Sets the list of namespace URIs to exclude from the Export + * + * @param excludeNamespaceURIs + */ + public void setExcludeNamespaceURIs(String[] excludeNamespaceURIs) + { + this.excludeNamespaceURIs = excludeNamespaceURIs; + } + + /** + * Gets the path to export from + * + * @return the path to export from + */ + public Location getExportFrom() + { + return exportFrom; + } + + /** + * Sets the path to export from + * + * @param exportFrom + */ + public void setExportFrom(Location exportFrom) + { + this.exportFrom = exportFrom; + } + +} diff --git a/source/java/org/alfresco/service/cmr/view/ExporterException.java b/source/java/org/alfresco/service/cmr/view/ExporterException.java new file mode 100644 index 0000000000..160a18443e --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ExporterException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the GNU Lesser General Public License as + * published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * You may obtain a copy of the License at + * + * http://www.gnu.org/licenses/lgpl.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + + +/** + * Base Exception of Export Exceptions. + * + * @author David Caruana + */ +public class ExporterException extends RuntimeException +{ + private static final long serialVersionUID = 3257008761007847733L; + + public ExporterException(String msg) + { + super(msg); + } + + public ExporterException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/service/cmr/view/ExporterService.java b/source/java/org/alfresco/service/cmr/view/ExporterService.java new file mode 100644 index 0000000000..0bb2b64d7c --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ExporterService.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import java.io.OutputStream; + + +/** + * Exporter Service + * + * @author David Caruana + */ +public interface ExporterService +{ + /** + * Export a view of the Repository using the default xml view schema. + * + * All repository information is exported to the single output stream. This means that any + * content properties are base64 encoded. + * + * @param viewWriter the output stream to export to + * @param parameters export parameters + * @param progress exporter callback for tracking progress of export + */ + public void exportView(OutputStream viewWriter, ExporterCrawlerParameters parameters, Exporter progress) + throws ExporterException; + + /** + * Export a view of the Repository using the default xml view schema. + * + * This export supports the custom handling of content properties. + * + * @param exportHandler the custom export handler for content properties + * @param parameters export parameters + * @param progress exporter callback for tracking progress of export + */ + public void exportView(ExportPackageHandler exportHandler, ExporterCrawlerParameters parameters, Exporter progress) + throws ExporterException; + + + /** + * Export a view of the Repository using a custom crawler and exporter. + * + * @param exporter custom exporter + * @param parameters export parameters + * @param progress exporter callback for tracking progress of export + */ + public void exportView(Exporter exporter, ExporterCrawlerParameters parameters, Exporter progress); + +} diff --git a/source/java/org/alfresco/service/cmr/view/ImportPackageHandler.java b/source/java/org/alfresco/service/cmr/view/ImportPackageHandler.java new file mode 100644 index 0000000000..4e581c6b80 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ImportPackageHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import java.io.InputStream; +import java.io.Reader; + + +/** + * Contract for a custom import package handler. + * + * @author David Caruana + */ +public interface ImportPackageHandler +{ + /** + * Start the Import + */ + public void startImport(); + + /** + * Get the package data stream + * + * @return the reader + */ + public Reader getDataStream(); + + /** + * Call-back for handling the import of content stream. + * + * @param content content descriptor + * @return the input stream onto the content + */ + public InputStream importStream(String content); + + /** + * End the Import + */ + public void endImport(); + +} diff --git a/source/java/org/alfresco/service/cmr/view/ImporterBinding.java b/source/java/org/alfresco/service/cmr/view/ImporterBinding.java new file mode 100644 index 0000000000..61f8089faa --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ImporterBinding.java @@ -0,0 +1,7 @@ +package org.alfresco.service.cmr.view; + +public interface ImporterBinding +{ + + public String getValue(String key); +} diff --git a/source/java/org/alfresco/service/cmr/view/ImporterException.java b/source/java/org/alfresco/service/cmr/view/ImporterException.java new file mode 100644 index 0000000000..e675ea3227 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ImporterException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + + +/** + * Base Exception of Import Exceptions. + * + * @author David Caruana + */ +public class ImporterException extends RuntimeException +{ + private static final long serialVersionUID = 3257008761007847733L; + + public ImporterException(String msg) + { + super(msg); + } + + public ImporterException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/service/cmr/view/ImporterProgress.java b/source/java/org/alfresco/service/cmr/view/ImporterProgress.java new file mode 100644 index 0000000000..e3fd91dd24 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ImporterProgress.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import java.io.Serializable; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Callback interface for monitoring progress of an import. + * + * @author David Caruana + * + */ +public interface ImporterProgress +{ + /** + * Report creation of a node. + * + * @param nodeRef the node ref + * @param parentRef the parent ref + * @param assocName the child association type name + * @param childName the child association name + */ + public void nodeCreated(NodeRef nodeRef, NodeRef parentRef, QName assocName, QName childName); + + /** + * Report creation of content + * + * @param nodeRef the node ref + * @param sourceUrl the source location of the content + */ + public void contentCreated(NodeRef nodeRef, String sourceUrl); + + /** + * Report setting of a property + * + * @param nodeRef the node ref + * @param property the property name + * @param value the property value + */ + public void propertySet(NodeRef nodeRef, QName property, Serializable value); + + /** + * Report addition of an aspect + * + * @param nodeRef the node ref + * @param aspect the aspect + */ + public void aspectAdded(NodeRef nodeRef, QName aspect); +} diff --git a/source/java/org/alfresco/service/cmr/view/ImporterService.java b/source/java/org/alfresco/service/cmr/view/ImporterService.java new file mode 100644 index 0000000000..5d68941897 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/ImporterService.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import java.io.Reader; + + +/** + * Importer Service. Entry point for importing xml data sources into the Repository. + * + * @author David Caruana + * + */ +public interface ImporterService +{ + + /** + * Import a Repository view into the specified location + * + * @param viewReader input stream containing the xml view to parse + * @param location the location to import under + * @param binding property values used for binding property place holders in import stream + * @param progress progress monitor (optional) + */ + public void importView(Reader viewReader, Location location, ImporterBinding binding, ImporterProgress progress) + throws ImporterException; + + + /** + * Import a Repository view into the specified location + * + * This import allows for a custom content importer. + * + * @param importHandler custom content importer + * @param location the location to import under + * @param binding property values used for binding property place holders in import stream + * @param progress progress monitor (optional) + */ + public void importView(ImportPackageHandler importHandler, Location location, ImporterBinding binding, ImporterProgress progress) + throws ImporterException; + +} diff --git a/source/java/org/alfresco/service/cmr/view/Location.java b/source/java/org/alfresco/service/cmr/view/Location.java new file mode 100644 index 0000000000..e6f149f64d --- /dev/null +++ b/source/java/org/alfresco/service/cmr/view/Location.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.view; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; + +/** + * Importer / Exporter Location + * + * @author David Caruana + */ +public class Location +{ + private StoreRef storeRef = null; + private NodeRef nodeRef = null; + private String path = null; + private QName childAssocType = null; + + + /** + * Construct + * + * @param nodeRef + */ + public Location(NodeRef nodeRef) + { + ParameterCheck.mandatory("Node Ref", nodeRef); + this.storeRef = nodeRef.getStoreRef(); + this.nodeRef = nodeRef; + } + + /** + * Construct + * + * @param storeRef + */ + public Location(StoreRef storeRef) + { + ParameterCheck.mandatory("Store Ref", storeRef); + this.storeRef = storeRef; + } + + /** + * @return the store ref + */ + public StoreRef getStoreRef() + { + return storeRef; + } + + /** + * @return the node ref + */ + public NodeRef getNodeRef() + { + return nodeRef; + } + + /** + * Sets the location to the specified path + * + * @param path path relative to store or node reference + */ + public void setPath(String path) + { + this.path = path; + } + + /** + * @return the location + */ + public String getPath() + { + return path; + } + + /** + * Sets the child association type + * + * @param childAssocType child association type + */ + public void setChildAssocType(QName childAssocType) + { + this.childAssocType = childAssocType; + } + + /** + * @return the child association type + */ + public QName getChildAssocType() + { + return childAssocType; + } +} diff --git a/source/java/org/alfresco/service/descriptor/Descriptor.java b/source/java/org/alfresco/service/descriptor/Descriptor.java new file mode 100644 index 0000000000..fb87cb2c62 --- /dev/null +++ b/source/java/org/alfresco/service/descriptor/Descriptor.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.descriptor; + + +/** + * Provides meta-data for the Alfresco stack. + * + * @author David Caruana + */ +public interface Descriptor +{ + /** + * Gets the major version number + * + * @return major version number + */ + public String getVersionMajor(); + + /** + * Gets the minor version number + * + * @return minor version number + */ + public String getVersionMinor(); + + /** + * Gets the version revision number + * + * @return revision number + */ + public String getVersionRevision(); + + /** + * Gets the version label + * + * @return the version label + */ + public String getVersionLabel(); + + /** + * Gets the full version number + * + * @return full version number as major.minor.revision (label) + */ + public String getVersion(); + + /** + * Gets the edition + * + * @return the edition + */ + public String getEdition(); + + /** + * Gets the list available descriptors + * + * @return descriptor keys + */ + public String[] getDescriptorKeys(); + + /** + * Get descriptor value + * + * @param key the descriptor key + * @return descriptor value (or null, if one not provided) + */ + public String getDescriptor(String key); + +} diff --git a/source/java/org/alfresco/service/descriptor/DescriptorService.java b/source/java/org/alfresco/service/descriptor/DescriptorService.java new file mode 100644 index 0000000000..4a7d42e5bc --- /dev/null +++ b/source/java/org/alfresco/service/descriptor/DescriptorService.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.descriptor; + + +/** + * Service for retrieving meta-data about Alfresco stack. + * + * @author David Caruana + * + */ +public interface DescriptorService +{ + /** + * Get descriptor for the server + * + * @return server descriptor + */ + public Descriptor getDescriptor(); + + /** + * Get descriptor for the repository + * + * @return repository descriptor + */ + public Descriptor getRepositoryDescriptor(); + +} diff --git a/source/java/org/alfresco/service/namespace/DynamicNameSpaceResolverTest.java b/source/java/org/alfresco/service/namespace/DynamicNameSpaceResolverTest.java new file mode 100644 index 0000000000..645140d1b4 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/DynamicNameSpaceResolverTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import junit.framework.TestCase; + +public class DynamicNameSpaceResolverTest extends TestCase +{ + + public DynamicNameSpaceResolverTest() + { + super(); + } + + public void testOne() + { + DynamicNamespacePrefixResolver dnpr = new DynamicNamespacePrefixResolver(null); + dnpr.registerNamespace("one", "http:/namespace/one"); + dnpr.registerNamespace("two", "http:/namespace/two"); + dnpr.registerNamespace("three", "http:/namespace/three"); + dnpr.registerNamespace("oneagain", "http:/namespace/one"); + dnpr.registerNamespace("four", "http:/namespace/one"); + dnpr.registerNamespace("four", "http:/namespace/four"); + + assertEquals("http:/namespace/one", dnpr.getNamespaceURI("one")); + assertEquals("http:/namespace/two", dnpr.getNamespaceURI("two")); + assertEquals("http:/namespace/three", dnpr.getNamespaceURI("three")); + assertEquals("http:/namespace/one", dnpr.getNamespaceURI("oneagain")); + assertEquals("http:/namespace/four", dnpr.getNamespaceURI("four")); + assertEquals(null, dnpr.getNamespaceURI("five")); + + dnpr.unregisterNamespace("four"); + assertEquals(null, dnpr.getNamespaceURI("four")); + + assertEquals(0, dnpr.getPrefixes("http:/namespace/four").size()); + assertEquals(1, dnpr.getPrefixes("http:/namespace/two").size()); + assertEquals(2, dnpr.getPrefixes("http:/namespace/one").size()); + + + } + + + public void testTwo() + { + DynamicNamespacePrefixResolver dnpr1 = new DynamicNamespacePrefixResolver(null); + dnpr1.registerNamespace("one", "http:/namespace/one"); + dnpr1.registerNamespace("two", "http:/namespace/two"); + dnpr1.registerNamespace("three", "http:/namespace/three"); + dnpr1.registerNamespace("oneagain", "http:/namespace/one"); + dnpr1.registerNamespace("four", "http:/namespace/one"); + dnpr1.registerNamespace("four", "http:/namespace/four"); + dnpr1.registerNamespace("five", "http:/namespace/five"); + dnpr1.registerNamespace("six", "http:/namespace/six"); + + DynamicNamespacePrefixResolver dnpr2 = new DynamicNamespacePrefixResolver(dnpr1); + dnpr2.registerNamespace("a", "http:/namespace/one"); + dnpr2.registerNamespace("b", "http:/namespace/two"); + dnpr2.registerNamespace("c", "http:/namespace/three"); + dnpr2.registerNamespace("d", "http:/namespace/one"); + dnpr2.registerNamespace("e", "http:/namespace/one"); + dnpr2.registerNamespace("f", "http:/namespace/four"); + dnpr2.registerNamespace("five", "http:/namespace/one"); + dnpr2.registerNamespace("six", "http:/namespace/seven"); + + assertEquals("http:/namespace/one", dnpr2.getNamespaceURI("one")); + assertEquals("http:/namespace/two", dnpr2.getNamespaceURI("two")); + assertEquals("http:/namespace/three", dnpr2.getNamespaceURI("three")); + assertEquals("http:/namespace/one", dnpr2.getNamespaceURI("oneagain")); + assertEquals("http:/namespace/four", dnpr2.getNamespaceURI("four")); + assertEquals("http:/namespace/one", dnpr2.getNamespaceURI("five")); + dnpr2.unregisterNamespace("five"); + + assertEquals("http:/namespace/five", dnpr2.getNamespaceURI("five")); + assertEquals("http:/namespace/one", dnpr2.getNamespaceURI("a")); + assertEquals("http:/namespace/two", dnpr2.getNamespaceURI("b")); + assertEquals("http:/namespace/three", dnpr2.getNamespaceURI("c")); + assertEquals("http:/namespace/one", dnpr2.getNamespaceURI("d")); + assertEquals("http:/namespace/one", dnpr2.getNamespaceURI("e")); + assertEquals("http:/namespace/four", dnpr2.getNamespaceURI("f")); + + assertEquals(5, dnpr2.getPrefixes("http:/namespace/one").size()); + assertEquals(2, dnpr2.getPrefixes("http:/namespace/two").size()); + assertEquals(2, dnpr2.getPrefixes("http:/namespace/three").size()); + assertEquals(2, dnpr2.getPrefixes("http:/namespace/four").size()); + assertEquals(1, dnpr2.getPrefixes("http:/namespace/five").size()); + assertEquals(0, dnpr2.getPrefixes("http:/namespace/six").size()); + assertEquals(1, dnpr2.getPrefixes("http:/namespace/seven").size()); + } + +} diff --git a/source/java/org/alfresco/service/namespace/DynamicNamespacePrefixResolver.java b/source/java/org/alfresco/service/namespace/DynamicNamespacePrefixResolver.java new file mode 100644 index 0000000000..4b0450d4f2 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/DynamicNamespacePrefixResolver.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * A delegating namespace prefix resolver which allows local over rides from the + * delegate. Allows standard/default prefixes to be available but over ridden as + * required. + * + * @author andyh + * + */ +public class DynamicNamespacePrefixResolver implements NamespaceService +{ + + /** + * The delegate + */ + private NamespacePrefixResolver delegate; + + /** + * The map uris keyed by prefix + */ + private HashMap map = new HashMap(); + + public DynamicNamespacePrefixResolver(NamespacePrefixResolver delegate) + { + super(); + this.delegate = delegate; + } + + public DynamicNamespacePrefixResolver() + { + this(null); + } + + /** + * Add prefix to name space mapping override + * + * @param prefix + * @param uri + */ + public void registerNamespace(String prefix, String uri) + { + map.put(prefix, uri); + } + + /** + * Remove a prefix to namespace mapping + * + * @param prefix + */ + public void unregisterNamespace(String prefix) + { + map.remove(prefix); + } + + // NameSpacePrefix Resolver + + public String getNamespaceURI(String prefix) throws NamespaceException + { + String uri = map.get(prefix); + if ((uri == null) && (delegate != null)) + { + uri = delegate.getNamespaceURI(prefix); + } + return uri; + } + + public Collection getPrefixes(String namespaceURI) throws NamespaceException + { + Collection prefixes = new ArrayList(); + for (String key : map.keySet()) + { + String uri = map.get(key); + if ((uri != null) && (uri.equals(namespaceURI))) + { + prefixes.add(key); + } + } + // Only add if not over ridden here (if identical already added) + if (delegate != null) + { + for (String prefix : delegate.getPrefixes(namespaceURI)) + { + if (!map.containsKey(prefix)) + { + prefixes.add(prefix); + } + } + } + return prefixes; + } + + public Collection getPrefixes() + { + Set prefixes = new HashSet(); + if(delegate != null) + { + prefixes.addAll(delegate.getPrefixes()); + } + prefixes.addAll(map.keySet()); + return prefixes; + } + + public Collection getURIs() + { + Set uris = new HashSet(); + if(delegate != null) + { + uris.addAll(delegate.getURIs()); + } + uris.addAll(map.keySet()); + return uris; + } + +} diff --git a/source/java/org/alfresco/service/namespace/InvalidQNameException.java b/source/java/org/alfresco/service/namespace/InvalidQNameException.java new file mode 100644 index 0000000000..7b71e59cf7 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/InvalidQNameException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + + +public class InvalidQNameException extends NamespaceException +{ + private static final long serialVersionUID = 7851788938794302629L; + + public InvalidQNameException(String msg) + { + super(msg); + } + + public InvalidQNameException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/service/namespace/NamespaceException.java b/source/java/org/alfresco/service/namespace/NamespaceException.java new file mode 100644 index 0000000000..2a2351a4f1 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/NamespaceException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + + +public class NamespaceException extends RuntimeException +{ + private static final long serialVersionUID = 7851788938794302629L; + + public NamespaceException(String msg) + { + super(msg); + } + + public NamespaceException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/service/namespace/NamespacePrefixResolver.java b/source/java/org/alfresco/service/namespace/NamespacePrefixResolver.java new file mode 100644 index 0000000000..5b52437754 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/NamespacePrefixResolver.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import java.util.Collection; + +/** + * The NamespacePrefixResolver provides a mapping between + * namespace prefixes and namespace URIs. + * + * @author David Caruana + */ +public interface NamespacePrefixResolver +{ + /** + * Gets the namespace URI registered for the given prefix + * + * @param prefix prefix to lookup + * @return the namespace + * @throws NamespaceException if prefix has not been registered + */ + public String getNamespaceURI(String prefix) + throws NamespaceException; + + /** + * Gets the registered prefixes for the given namespace URI + * + * @param namespaceURI namespace URI to lookup + * @return the prefixes (or empty collection, if no prefixes registered against URI) + * @throws NamespaceException if URI has not been registered + */ + public Collection getPrefixes(String namespaceURI) + throws NamespaceException; + + /** + * Gets all registered Prefixes + * + * @return collection of all registered namespace prefixes + */ + Collection getPrefixes(); + + /** + * Gets all registered Uris + * + * @return collection of all registered namespace uris + */ + Collection getURIs(); + +} diff --git a/source/java/org/alfresco/service/namespace/NamespaceService.java b/source/java/org/alfresco/service/namespace/NamespaceService.java new file mode 100644 index 0000000000..6ee1fda59a --- /dev/null +++ b/source/java/org/alfresco/service/namespace/NamespaceService.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + + + +/** + * Namespace Service. + * + * The Namespace Service provides access to and definition of namespace + * URIs and Prefixes. + * + * @author David Caruana + */ +public interface NamespaceService extends NamespacePrefixResolver +{ + /** Default Namespace URI */ + public static final String DEFAULT_URI = ""; + + /** Default Namespace Prefix */ + public static final String DEFAULT_PREFIX = ""; + + /** Default Alfresco URI */ + public static final String ALFRESCO_URI = "http://www.alfresco.org"; + + /** Default Alfresco Prefix */ + public static final String ALFRESCO_PREFIX = "alf"; + + /** Dictionary Model URI */ + public static final String DICTIONARY_MODEL_1_0_URI = "http://www.alfresco.org/model/dictionary/1.0"; + + /** Dictionary Model Prefix */ + public static final String DICTIONARY_MODEL_PREFIX = "d"; + + /** System Model URI */ + public static final String SYSTEM_MODEL_1_0_URI = "http://www.alfresco.org/model/system/1.0"; + + /** System Model Prefix */ + public static final String SYSTEM_MODEL_PREFIX = "sys"; + + /** Content Model URI */ + public static final String CONTENT_MODEL_1_0_URI = "http://www.alfresco.org/model/content/1.0"; + + /** Content Model Prefix */ + public static final String CONTENT_MODEL_PREFIX = "cm"; + + /** Application Model URI */ + public static final String APP_MODEL_1_0_URI = "http://www.alfresco.org/model/application/1.0"; + + /** Application Model Prefix */ + public static final String APP_MODEL_PREFIX = "app"; + + /** Alfresco View Namespace URI */ + public static final String REPOSITORY_VIEW_1_0_URI = "http://www.alfresco.org/view/repository/1.0"; + + /** Alfresco View Namespace Prefix */ + public static final String REPOSITORY_VIEW_PREFIX = "view"; + + /** Alfresco security URI */ + public static final String SECURITY_MODEL_1_0_URI = "http://www.alfresco.org/model/security/1.0"; + + /** Alfresco security Prefix */ + public static final String SECURITY_MODEL_PREFIX = "security"; + + + /** + * Register a prefix for namespace uri. + * + * @param prefix + * @param uri + */ + public void registerNamespace(String prefix, String uri); + + + /** + * Unregister a prefix. + * + * @param prefix + */ + public void unregisterNamespace(String prefix); + +} diff --git a/source/java/org/alfresco/service/namespace/QName.java b/source/java/org/alfresco/service/namespace/QName.java new file mode 100644 index 0000000000..61c9bf8ed2 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/QName.java @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import java.io.Serializable; +import java.util.Collection; + +/** + * QName represents the qualified name of a Repository item. Each + * QName consists of a local name qualified by a namespace. + *

    + * The {@link org.alfresco.service.namespace.QNamePattern QNamePattern} is implemented + * to allow instances of this class to be used for direct pattern matching where + * required on interfaces. + * + * @author David Caruana + * + */ +public final class QName implements QNamePattern, Serializable, Cloneable +{ + private static final long serialVersionUID = 3977016258204348976L; + + private String namespaceURI; // never null + private String localName; // never null + private int hashCode; + private String prefix; + + public static final char NAMESPACE_PREFIX = ':'; + public static final char NAMESPACE_BEGIN = '{'; + public static final char NAMESPACE_END = '}'; + public static final int MAX_LENGTH = 100; + + + /** + * Create a QName + * + * @param namespaceURI the qualifying namespace (maybe null or empty string) + * @param localName the qualified name + * @return the QName + */ + public static QName createQName(String namespaceURI, String localName) + throws InvalidQNameException + { + if (localName == null || localName.length() == 0) + { + throw new InvalidQNameException("A QName must consist of a local name"); + } + return new QName(namespaceURI, localName, null); + } + + + /** + * Create a QName + * + * @param prefix namespace prefix (maybe null or empty string) + * @param localName local name + * @param prefixResolver lookup to resolve mappings between prefix and namespace + * @return the QName + */ + public static QName createQName(String prefix, String localName, NamespacePrefixResolver prefixResolver) + throws InvalidQNameException, NamespaceException + { + // Validate Arguments + if (localName == null || localName.length() == 0) + { + throw new InvalidQNameException("A QName must consist of a local name"); + } + if (prefixResolver == null) + { + throw new IllegalArgumentException("A Prefix Resolver must be specified"); + } + if (prefix == null) + { + prefix = NamespaceService.DEFAULT_PREFIX; + } + + // Calculate namespace URI and create QName + String uri = prefixResolver.getNamespaceURI(prefix); + if (uri == null) + { + throw new NamespaceException("Namespace prefix " + prefix + " is not mapped to a namespace URI"); + } + return new QName(uri, localName, prefix); + } + + + /** + * Create a QName + * + * @param qname qualified name of the following format prefix:localName + * @param prefixResolver lookup to resolve mappings between prefix and namespace + * @return the QName + */ + public static QName createQName(String qname, NamespacePrefixResolver prefixResolver) + throws InvalidQNameException, NamespaceException + { + QName name = null; + if (qname != null) + { + int colonIndex = qname.indexOf(NAMESPACE_PREFIX); + String prefix = (colonIndex == -1) ? NamespaceService.DEFAULT_PREFIX : qname.substring(0, colonIndex); + String localName = (colonIndex == -1) ? qname : qname.substring(colonIndex +1); + name = createQName(prefix, localName, prefixResolver); + } + return name; + } + + + /** + * Create a QName from its internal string representation of the following format: + * + * {namespaceURI}localName + * + * @param qname the string representation of the QName + * @return the QName + * @throws IllegalArgumentException + * @throws InvalidQNameException + */ + public static QName createQName(String qname) + throws InvalidQNameException + { + if (qname == null || qname.length() == 0) + { + throw new InvalidQNameException("Argument qname is mandatory"); + } + + String namespaceURI = null; + String localName = null; + + // Parse namespace + int namespaceBegin = qname.indexOf(NAMESPACE_BEGIN); + int namespaceEnd = -1; + if (namespaceBegin != -1) + { + if (namespaceBegin != 0) + { + throw new InvalidQNameException("QName '" + qname + "' must start with a namespaceURI"); + } + namespaceEnd = qname.indexOf(NAMESPACE_END, namespaceBegin + 1); + if (namespaceEnd == -1) + { + throw new InvalidQNameException("QName '" + qname + "' is missing the closing namespace " + NAMESPACE_END + " token"); + } + namespaceURI = qname.substring(namespaceBegin + 1, namespaceEnd); + } + + // Parse name + localName = qname.substring(namespaceEnd + 1); + if (localName == null || localName.length() == 0) + { + throw new InvalidQNameException("QName '" + qname + "' must consist of a local name"); + } + + // Construct QName + return new QName(namespaceURI, localName, null); + } + + + /** + * Create a valid local name from the specified name + * + * @param name name to create valid local name from + * @return valid local name + */ + public static String createValidLocalName(String name) + { + // Validate length + if (name == null || name.length() == 0) + { + throw new IllegalArgumentException("Local name cannot be null or empty."); + } + if (name.length() > MAX_LENGTH) + { + name = name.substring(0, MAX_LENGTH); + } + + return name; + } + + + /** + * Create a QName + * + * @param qname qualified name of the following format prefix:localName + * @return string array where index 0 => prefix and index 1 => local name + */ + public static String[] splitPrefixedQName(String qname) + throws InvalidQNameException, NamespaceException + { + if (qname != null) + { + int colonIndex = qname.indexOf(NAMESPACE_PREFIX); + String prefix = (colonIndex == -1) ? NamespaceService.DEFAULT_PREFIX : qname.substring(0, colonIndex); + String localName = (colonIndex == -1) ? qname : qname.substring(colonIndex +1); + return new String[] { prefix, localName }; + } + return null; + } + + + /** + * Construct QName + * + * @param namespace qualifying namespace (maybe null or empty string) + * @param name qualified name + * @param prefix prefix (maybe null or empty string) + */ + private QName(String namespace, String name, String prefix) + { + this.namespaceURI = (namespace == null) ? NamespaceService.DEFAULT_URI : namespace; + this.prefix = prefix; + this.localName = name; + this.hashCode = 0; + } + + @Override + public Object clone() throws CloneNotSupportedException + { + return super.clone(); + } + + /** + * Gets the name + * + * @return the name + */ + public String getLocalName() + { + return this.localName; + } + + + /** + * Gets the namespace + * + * @return the namespace (empty string when not specified, but never null) + */ + public String getNamespaceURI() + { + return this.namespaceURI; + } + + + /** + * Gets a prefix resolved version of this QName + * + * @param resolver namespace prefix resolver + * @return QName with prefix resolved + */ + public QName getPrefixedQName(NamespacePrefixResolver resolver) + { + Collection prefixes = resolver.getPrefixes(namespaceURI); + if (prefixes.size() == 0) + { + throw new NamespaceException("A namespace prefix is not registered for uri " + namespaceURI); + } + String resolvedPrefix = prefixes.iterator().next(); + if (prefix != null && prefix.equals(resolvedPrefix)) + { + return this; + } + return new QName(namespaceURI, localName, resolvedPrefix); + } + + + /** + * Two QNames are equal only when both their name and namespace match. + * + * Note: The prefix is ignored during the comparison. + */ + public boolean equals(Object object) + { + if (this == object) + { + return true; + } + else if (object == null) + { + return false; + } + if (object instanceof QName) + { + QName other = (QName) object; + // namespaceURI and localname are not allowed to be null + return (this.namespaceURI.equals(other.namespaceURI) && + this.localName.equals(other.localName)); + } + else + { + return false; + } + } + + /** + * Performs a direct comparison between qnames. + * + * @see #equals(Object) + */ + public boolean isMatch(QName qname) + { + return this.equals(qname); + } + + /** + * Calculate hashCode. Follows pattern used by String where hashCode is + * cached (QName is immutable). + */ + public int hashCode() + { + if (this.hashCode == 0) + { + // the hashcode assignment is atomic - it is only an integer + this.hashCode = ((37 * localName.hashCode()) + namespaceURI.hashCode()); + } + return this.hashCode; + } + + + /** + * Render string representation of QName using format: + * + * {namespace}name + * + * @return the string representation + */ + public String toString() + { + return NAMESPACE_BEGIN + namespaceURI + NAMESPACE_END + localName; + } + + + /** + * Render string representation of QName using format: + * + * prefix:name + * + * @return the string representation + */ + public String toPrefixString() + { + return (prefix == null) ? localName : prefix + NAMESPACE_PREFIX + localName; + } + + + /** + * Render string representation of QName using format: + * + * prefix:name + * + * according to namespace prefix mappings of specified namespace resolver. + * + * @param prefixResolver namespace prefix resolver + * + * @return the string representation + */ + public String toPrefixString(NamespacePrefixResolver prefixResolver) + { + Collection prefixes = prefixResolver.getPrefixes(namespaceURI); + if (prefixes.size() == 0) + { + throw new NamespaceException("A namespace prefix is not registered for uri " + namespaceURI); + } + String prefix = prefixes.iterator().next(); + if (prefix.equals(NamespaceService.DEFAULT_PREFIX)) + { + return localName; + } + else + { + return prefix + NAMESPACE_PREFIX + localName; + } + } + + + /** + * Creates a QName representation for the given String. If the String has no namespace the Alfresco namespace is + * added. If the String has a prefix an attempt to resolve the prefix to the full URI will be made. + * + * @param str The string to convert + * @return A QName representation of the given string + */ + public static QName resolveToQName(NamespacePrefixResolver prefixResolver, String str) + { + QName qname = null; + + if (str == null && str.length() == 0) + { + throw new IllegalArgumentException("str parameter is mandatory"); + } + + if (str.charAt(0) == (NAMESPACE_BEGIN)) + { + // create QName directly + qname = createQName(str); + } + else if (str.indexOf(NAMESPACE_PREFIX) != -1) + { + // extract the prefix and try and resolve using the + // namespace service + int end = str.indexOf(NAMESPACE_PREFIX); + String prefix = str.substring(0, end); + String localName = str.substring(end + 1); + String uri = prefixResolver.getNamespaceURI(prefix); + + if (uri != null) + { + qname = createQName(uri, localName); + } + } + else + { + // there's no namespace so prefix with Alfresco's Content Model + qname = createQName(NamespaceService.CONTENT_MODEL_1_0_URI, str); + } + + return qname; + } + + + /** + * Creates a string representation of a QName for the given string. If the given string already has a namespace, + * either a URL or a prefix, nothing the given string is returned. If it does not have a namespace the Alfresco + * namespace is added. + * + * @param str + * The string to convert + * + * @return A QName String representation of the given string + */ + public static String resolveToQNameString(NamespacePrefixResolver prefixResolver, String str) + { + String result = str; + + if (str == null && str.length() == 0) + { + throw new IllegalArgumentException("str parameter is mandatory"); + } + + if (str.charAt(0) != NAMESPACE_BEGIN && str.indexOf(NAMESPACE_PREFIX) != -1) + { + // get the prefix and resolve to the uri + int end = str.indexOf(NAMESPACE_PREFIX); + String prefix = str.substring(0, end); + String localName = str.substring(end + 1); + String uri = prefixResolver.getNamespaceURI(prefix); + + if (uri != null) + { + result = new StringBuilder(64).append(NAMESPACE_BEGIN).append(uri).append(NAMESPACE_END).append( + localName).toString(); + } + } + else if (str.charAt(0) != NAMESPACE_BEGIN) + { + // there's no namespace so prefix with Alfresco's Content Model + result = new StringBuilder(64).append(NAMESPACE_BEGIN).append(NamespaceService.CONTENT_MODEL_1_0_URI) + .append(NAMESPACE_END).append(str).toString(); + } + + return result; + } +} diff --git a/source/java/org/alfresco/service/namespace/QNameMap.java b/source/java/org/alfresco/service/namespace/QNameMap.java new file mode 100644 index 0000000000..b0bdaae62d --- /dev/null +++ b/source/java/org/alfresco/service/namespace/QNameMap.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.util.ApplicationContextHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A Map that holds as it's key a QName stored in it's internal String representation. + * Calls to get and put automatically map the key to and from the QName representation. + * + * @author gavinc + */ +public class QNameMap implements Map, Cloneable +{ + protected static Log logger = LogFactory.getLog(QNameMap.class); + protected Map contents = new HashMap(11, 1.0f); + protected NamespacePrefixResolver resolver = null; + + /** + * Constructor + * + * @param resolver Mandatory NamespacePrefixResolver helper + */ + public QNameMap(NamespacePrefixResolver resolver) + { + if (resolver == null) + { + throw new IllegalArgumentException("NamespacePrefixResolver is mandatory."); + } + this.resolver = resolver; + } + + /** + * @see java.util.Map#size() + */ + public final int size() + { + return this.contents.size(); + } + + /** + * @see java.util.Map#isEmpty() + */ + public boolean isEmpty() + { + return this.contents.isEmpty(); + } + + /** + * @see java.util.Map#containsKey(java.lang.Object) + */ + public boolean containsKey(Object key) + { + return (this.contents.containsKey(QName.resolveToQNameString(resolver, (String)key))); + } + + /** + * @see java.util.Map#containsValue(java.lang.Object) + */ + public boolean containsValue(Object value) + { + return this.contents.containsValue(value); + } + + /** + * @see java.util.Map#get(java.lang.Object) + */ + public Object get(Object key) + { + String qnameKey = QName.resolveToQNameString(resolver, key.toString()); + Object obj = this.contents.get(qnameKey); + + return obj; + } + + /** + * @see java.util.Map#put(K, V) + */ + public Object put(Object key, Object value) + { + return this.contents.put(QName.resolveToQNameString(resolver, (String)key), value); + } + + /** + * @see java.util.Map#remove(java.lang.Object) + */ + public Object remove(Object key) + { + return this.contents.remove(QName.resolveToQNameString(resolver, (String)key)); + } + + /** + * @see java.util.Map#putAll(java.util.Map) + */ + public void putAll(Map t) + { + for (Object key : t.keySet()) + { + this.put(key, t.get(key)); + } + } + + /** + * @see java.util.Map#clear() + */ + public void clear() + { + this.contents.clear(); + } + + /** + * @see java.util.Map#keySet() + */ + public Set keySet() + { + return this.contents.keySet(); + } + + /** + * @see java.util.Map#values() + */ + public Collection values() + { + return this.contents.values(); + } + + /** + * @see java.util.Map#entrySet() + */ + public Set entrySet() + { + return this.contents.entrySet(); + } + + /** + * Override Object.toString() to provide useful debug output + */ + public String toString() + { + return this.contents.toString(); + } + + /** + * Shallow copy the map by copying keys and values into a new QNameMap + */ + public Object clone() + { + QNameMap map = new QNameMap(resolver); + map.putAll(this); + + return map; + } +} diff --git a/source/java/org/alfresco/service/namespace/QNamePattern.java b/source/java/org/alfresco/service/namespace/QNamePattern.java new file mode 100644 index 0000000000..c09cb19a87 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/QNamePattern.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + + +/** + * Provides pattern matching against {@link org.alfresco.service.namespace.QName qnames}. + *

    + * Implementations will use different mechanisms to match against the + * {@link org.alfresco.service.namespace.QName#getNamespaceURI() namespace} and + * {@link org.alfresco.service.namespace.QName#getLocalName()() localname}. + * + * @see org.alfresco.service.namespace.QName + * + * @author Derek Hulley + */ +public interface QNamePattern +{ + /** + * Checks if the given qualified name matches the pattern represented + * by this instance + * + * @param qname the instance to check + * @return Returns true if the qname matches this pattern + */ + public boolean isMatch(QName qname); +} diff --git a/source/java/org/alfresco/service/namespace/QNamePatternTest.java b/source/java/org/alfresco/service/namespace/QNamePatternTest.java new file mode 100644 index 0000000000..bc41771f29 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/QNamePatternTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + + +import junit.framework.TestCase; + +/** + * Tests the various implementations of the + * {@link org.alfresco.service.namespace.QNamePattern}. + * + * @author Derek Hulley + */ +public class QNamePatternTest extends TestCase +{ + private static final String TEST_NAMESPACE = "http://www.alfresco.org/QNamePatternTest"; + + QName check1; + QName check2; + QName check3; + + public QNamePatternTest(String name) + { + super(name); + } + + public void setUp() throws Exception + { + check1 = QName.createQName(null, "ABC"); + check2 = QName.createQName(TEST_NAMESPACE, "XYZ"); + check3 = QName.createQName(TEST_NAMESPACE, "ABC"); + } + + public void testSimpleQNamePattern() throws Exception + { + QNamePattern pattern = QName.createQName(TEST_NAMESPACE, "ABC"); + + // check + assertFalse("Simple match failed: " + check1, pattern.isMatch(check1)); + assertFalse("Simple match failed: " + check2, pattern.isMatch(check2)); + assertTrue("Simple match failed: " + check3, pattern.isMatch(check3)); + } + + public void testRegexQNamePatternMatcher() throws Exception + { + QNamePattern pattern = new RegexQNamePattern(".*alfresco.*", "A.?C"); + + // check + assertFalse("Regex match failed: " + check1, pattern.isMatch(check1)); + assertFalse("Regex match failed: " + check2, pattern.isMatch(check2)); + assertTrue("Regex match failed: " + check3, pattern.isMatch(check3)); + + assertTrue("All match failed: " + check1, RegexQNamePattern.MATCH_ALL.isMatch(check1)); + assertTrue("All match failed: " + check2, RegexQNamePattern.MATCH_ALL.isMatch(check2)); + assertTrue("All match failed: " + check3, RegexQNamePattern.MATCH_ALL.isMatch(check3)); + } +} diff --git a/source/java/org/alfresco/service/namespace/QNameTest.java b/source/java/org/alfresco/service/namespace/QNameTest.java new file mode 100644 index 0000000000..7a58d707c3 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/QNameTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import java.util.Collection; +import java.util.HashSet; + +import junit.framework.TestCase; + + + +/** + * @see org.alfresco.service.namespace.QName + * + * @author David Caruana + */ +public class QNameTest extends TestCase +{ + + public QNameTest(String name) + { + super(name); + } + + + public void testInvalidQName() throws Exception + { + try + { + QName qname = QName.createQName(""); + fail("Missing local name was not caught"); + } + catch (InvalidQNameException e) + { + } + + try + { + QName qname = QName.createQName("invalid{}name"); + fail("Namespace not at start was not caught"); + } + catch (InvalidQNameException e) + { + } + + try + { + QName qname = QName.createQName("{name"); + fail("Missing closing namespace token was not caught"); + } + catch (InvalidQNameException e) + { + } + + try + { + QName qname = QName.createQName("{}"); + fail("Missing local name after namespace was not caught"); + } + catch (InvalidQNameException e) + { + } + + try + { + QName qname = QName.createQName("{}name"); + } + catch (InvalidQNameException e) + { + fail("Empty namespace is valid"); + } + + try + { + QName qname = QName.createQName("{namespace}name"); + assertEquals("namespace", qname.getNamespaceURI()); + assertEquals("name", qname.getLocalName()); + } + catch (InvalidQNameException e) + { + fail("Valid namespace has been thrown out"); + } + + try + { + QName qname = QName.createQName((String) null, (String) null); + fail("Null name was not caught"); + } + catch (InvalidQNameException e) + { + } + + try + { + QName qname = QName.createQName((String) null, ""); + fail("Empty name was not caught"); + } + catch (InvalidQNameException e) + { + } + + } + + + public void testConstruction() + { + QName qname1 = QName.createQName("namespace1", "name1"); + assertEquals("namespace1", qname1.getNamespaceURI()); + assertEquals("name1", qname1.getLocalName()); + + QName qname2 = QName.createQName("{namespace2}name2"); + assertEquals("namespace2", qname2.getNamespaceURI()); + assertEquals("name2", qname2.getLocalName()); + + QName qname3 = QName.createQName(null, "name3"); + assertEquals("", qname3.getNamespaceURI()); + + QName qname4 = QName.createQName("", "name4"); + assertEquals("", qname4.getNamespaceURI()); + + QName qname5 = QName.createQName("{}name5"); + assertEquals("", qname5.getNamespaceURI()); + + QName qname6 = QName.createQName("name6"); + assertEquals("", qname6.getNamespaceURI()); + } + + + public void testStringRepresentation() + { + QName qname1 = QName.createQName("namespace", "name1"); + assertEquals("{namespace}name1", qname1.toString()); + + QName qname2 = QName.createQName("", "name2"); + assertEquals("{}name2", qname2.toString()); + + QName qname3 = QName.createQName("{namespace}name3"); + assertEquals("{namespace}name3", qname3.toString()); + + QName qname4 = QName.createQName("{}name4"); + assertEquals("{}name4", qname4.toString()); + + QName qname5 = QName.createQName("name5"); + assertEquals("{}name5", qname5.toString()); + } + + + public void testEquality() + { + QName qname1 = QName.createQName("namespace", "name"); + QName qname2 = QName.createQName("namespace", "name"); + QName qname3 = QName.createQName("{namespace}name"); + assertEquals(qname1, qname2); + assertEquals(qname1, qname3); + assertEquals(qname1.hashCode(), qname2.hashCode()); + assertEquals(qname1.hashCode(), qname3.hashCode()); + + QName qname4 = QName.createQName("", "name"); + QName qname5 = QName.createQName("", "name"); + QName qname6 = QName.createQName(null, "name"); + assertEquals(qname4, qname5); + assertEquals(qname4, qname6); + assertEquals(qname4.hashCode(), qname5.hashCode()); + assertEquals(qname4.hashCode(), qname6.hashCode()); + + QName qname7 = QName.createQName("namespace", "name"); + QName qname8 = QName.createQName("namespace", "differentname"); + assertFalse(qname7.equals(qname8)); + assertFalse(qname7.hashCode() == qname8.hashCode()); + + QName qname9 = QName.createQName("namespace", "name"); + QName qname10 = QName.createQName("differentnamespace", "name"); + assertFalse(qname9.equals(qname10)); + assertFalse(qname9.hashCode() == qname10.hashCode()); + } + + + public void testPrefix() + { + try + { + QName noResolver = QName.createQName(NamespaceService.ALFRESCO_PREFIX, "alfresco prefix", null); + fail("Null resolver was not caught"); + } + catch (IllegalArgumentException e) + { + } + + NamespacePrefixResolver mockResolver = new MockNamespacePrefixResolver(); + QName qname1 = QName.createQName(NamespaceService.ALFRESCO_PREFIX, "alfresco prefix", mockResolver); + assertEquals(NamespaceService.ALFRESCO_URI, qname1.getNamespaceURI()); + QName qname2 = QName.createQName("", "default prefix", mockResolver); + assertEquals(NamespaceService.DEFAULT_URI, qname2.getNamespaceURI()); + QName qname3 = QName.createQName(null, "null default prefix", mockResolver); + assertEquals(NamespaceService.DEFAULT_URI, qname3.getNamespaceURI()); + + try + { + QName qname4 = QName.createQName("garbage", "garbage prefix", mockResolver); + fail("Invalid Prefix was not caught"); + } + catch (NamespaceException e) + { + } + } + + + private static class MockNamespacePrefixResolver + implements NamespacePrefixResolver + { + + public String getNamespaceURI(String prefix) + { + if (prefix.equals(NamespaceService.DEFAULT_PREFIX)) + { + return NamespaceService.DEFAULT_URI; + } + else if (prefix.equals(NamespaceService.ALFRESCO_PREFIX)) + { + return NamespaceService.ALFRESCO_URI; + } + throw new NamespaceException("Prefix " + prefix + " not registered"); + } + + public Collection getPrefixes(String namespaceURI) + { + throw new NamespaceException("URI " + namespaceURI + " not registered"); + } + + public Collection getPrefixes() + { + HashSet prefixes = new HashSet(); + prefixes.add(NamespaceService.DEFAULT_PREFIX); + prefixes.add(NamespaceService.ALFRESCO_PREFIX); + return prefixes; + } + + public Collection getURIs() + { + HashSet uris = new HashSet(); + uris.add(NamespaceService.DEFAULT_URI); + uris.add(NamespaceService.ALFRESCO_URI); + return uris; + } + + } + +} diff --git a/source/java/org/alfresco/service/namespace/RegexQNamePattern.java b/source/java/org/alfresco/service/namespace/RegexQNamePattern.java new file mode 100644 index 0000000000..506798db39 --- /dev/null +++ b/source/java/org/alfresco/service/namespace/RegexQNamePattern.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.namespace; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Provides matching between {@link org.alfresco.service.namespace.QName qnames} using + * regular expression matching. + *

    + * A simple {@link #MATCH_ALL convenience} pattern matcher is also provided that + * will match any qname. + * + * @see java.lang.String#matches(java.lang.String) + * + * @author Derek Hulley + */ +public class RegexQNamePattern implements QNamePattern +{ + private static final Log logger = LogFactory.getLog(RegexQNamePattern.class); + + /** A helper pattern matcher that will match all qnames */ + public static final QNamePattern MATCH_ALL = new QNamePattern() + { + public boolean isMatch(QName qname) + { + return true; + } + }; + + private String namespaceUriPattern; + private String localNamePattern; + private String combinedPattern; + + /** + * @param namespaceUriPattern a regex pattern that will be applied to the namespace URI + * @param localNamePattern a regex pattern that will be applied to the local name + */ + public RegexQNamePattern(String namespaceUriPattern, String localNamePattern) + { + this.namespaceUriPattern = namespaceUriPattern; + this.localNamePattern = localNamePattern; + this.combinedPattern = null; + } + + /** + * @param combinedPattern a regex pattern that will be applied to the full qname + * string representation + * + * @see QName#toString() + */ + public RegexQNamePattern(String combinedPattern) + { + this.combinedPattern = combinedPattern; + this.namespaceUriPattern = null; + this.localNamePattern = null; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(56); + sb.append("RegexQNamePattern["); + if (combinedPattern != null) + { + sb.append(" pattern=").append(combinedPattern); + } + else + { + sb.append(" uri=").append(namespaceUriPattern); + sb.append(", localname=").append(namespaceUriPattern); + } + sb.append(" ]"); + return sb.toString(); + } + + /** + * @param qname the value to check against this pattern + * @return Returns true if the regex pattern provided match thos of the provided qname + */ + public boolean isMatch(QName qname) + { + boolean match = false; + if (combinedPattern != null) + { + String qnameStr = qname.toString(); + match = qnameStr.matches(combinedPattern); + } + else + { + match = (qname.getNamespaceURI().matches(namespaceUriPattern) && + qname.getLocalName().matches(localNamePattern)); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("QName matching: \n" + + " matcher: " + this + "\n" + + " qname: " + qname + "\n" + + " result: " + match); + } + return match; + } +} diff --git a/source/java/org/alfresco/service/transaction/TransactionService.java b/source/java/org/alfresco/service/transaction/TransactionService.java new file mode 100644 index 0000000000..9cdc05f7c1 --- /dev/null +++ b/source/java/org/alfresco/service/transaction/TransactionService.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.transaction; + +import javax.transaction.UserTransaction; + +/** + * Contract for retrieving access to a user transaction. + *

    + * Note that the implementation of the {@link javax.transaction.UserTransaction} + * is not able to provide the full set of status codes available on the + * {@link javax.transaction.Status} class. + * + * @author David Caruana + */ +public interface TransactionService +{ + /** + * Determine if ALL user transactions will be read-only. + * + * @return Returns true if all transactions are read-only. + */ + public boolean isReadOnly(); + + /** + * Gets a user transaction that supports transaction propagation. + * This is like the EJB REQUIRED transaction attribute. + * + * @return the user transaction + */ + UserTransaction getUserTransaction(); + + /** + * Gets a user transaction that supports transaction propagation. + * This is like the EJB REQUIRED transaction attribute. + * + * @param readonly Set true for a READONLY transaction instance, false otherwise. + * Note that it is not always possible to force a write transaction if the + * system is in read-only mode. + * + * @return the user transaction + */ + UserTransaction getUserTransaction(boolean readonly); + + /** + * Gets a user transaction that ensures a new transaction is created. + * Any enclosing transaction is not propagated. + * This is like the EJB REQUIRES_NEW transaction attribute - + * when the transaction is started, the current transaction will be + * suspended and a new one started. + * + * @return Returns a non-gating user transaction + */ + UserTransaction getNonPropagatingUserTransaction(); +} diff --git a/source/java/org/alfresco/tools/Export.java b/source/java/org/alfresco/tools/Export.java new file mode 100644 index 0000000000..da6bb3b67e --- /dev/null +++ b/source/java/org/alfresco/tools/Export.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.tools; + +import java.io.File; +import java.io.InputStream; +import java.util.Collection; + +import org.alfresco.repo.exporter.FileExportPackageHandler; +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ExportPackageHandler; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterException; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; + + +/** + * Alfresco Repository Export Tool + * + * @author David Caruana + */ +public final class Export extends Tool +{ + /** Export Context */ + private ExportContext context; + + /** + * Entry Point + * + * @param args + */ + public static void main(String[] args) + { + Tool tool = new Export(); + tool.start(args); + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#getToolName() + */ + @Override + String getToolName() + { + return "Alfresco Repository Exporter"; + } + + /** + * Process Export Tool command line arguments + * + * @param args the arguments + * @return the export context + */ + @Override + /*package*/ ToolContext processArgs(String[] args) + { + context = new ExportContext(); + context.setLogin(true); + + int i = 0; + while (i < args.length) + { + if (args[i].equals("-h") || args[i].equals("-help")) + { + context.setHelp(true); + break; + } + else if (args[i].equals("-s") || args[i].equals("-store")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the parameter -store must be specified"); + } + context.storeRef = new StoreRef(args[i]); + } + else if (args[i].equals("-p") || args[i].equals("-path")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the parameter -path must be specified"); + } + context.path = args[i]; + } + else if (args[i].equals("-d") || args[i].equals("-dir")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value

    for the parameter -dir must be specified"); + } + context.destDir = args[i]; + } + else if (args[i].equals("-packagedir")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the parameter -packagedir must be specified"); + } + context.packageDir = args[i]; + } + else if (args[i].equals("-user")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -user must be specified"); + } + context.setUsername(args[i]); + } + else if (args[i].equals("-pwd")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -pwd must be specified"); + } + context.setPassword(args[i]); + } + else if (args[i].equals("-root")) + { + context.self = true; + } + else if (args[i].equals("-nochildren")) + { + context.children = false; + } + else if (args[i].equals("-zip")) + { + context.zipped = true; + } + else if (args[i].equals("-overwrite")) + { + context.overwrite = true; + } + else if (args[i].equals("-quiet")) + { + context.setQuiet(true); + } + else if (args[i].equals("-verbose")) + { + context.setVerbose(true); + } + else if (i == (args.length - 1)) + { + context.packageName = args[i]; + } + else + { + throw new ToolException("Unknown option " + args[i]); + } + + // next argument + i++; + } + + return context; + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#displayHelp() + */ + @Override + /*package*/ void displayHelp() + { + System.out.println("Usage: export -user username -s[tore] store [options] packagename"); + System.out.println(""); + System.out.println("username: username for login"); + System.out.println("store: the store to extract from in the form of scheme://store_name"); + System.out.println("packagename: the filename to export to (with or without extension)"); + System.out.println(""); + System.out.println("Options:"); + System.out.println(" -h[elp] display this help"); + System.out.println(" -p[ath] the path within the store to extract from (default: /)"); + System.out.println(" -d[ir] the destination directory to export to (default: current directory)"); + System.out.println(" -pwd password for login"); + System.out.println(" -packagedir the directory to place extracted content (default: dir/)"); + System.out.println(" -root extract the item located at export path"); + System.out.println(" -nochildren do not extract children of the item at export path"); + System.out.println(" -overwrite force overwrite of existing export package if it already exists"); + System.out.println(" -quiet do not display any messages during export"); + System.out.println(" -verbose report export progress"); + System.out.println(" -zip export in zip format"); + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#execute() + */ + @Override + void execute() throws ToolException + { + ExporterService exporter = getServiceRegistry().getExporterService(); + + // create export package handler + ExportPackageHandler exportHandler = null; + if (context.zipped) + { + exportHandler = new ZipHandler(context.getDestDir(), context.getZipFile(), context.getPackageFile(), context.getPackageDir(), context.overwrite); + } + else + { + exportHandler = new FileHandler(context.getDestDir(), context.getPackageFile(), context.getPackageDir(), context.overwrite); + } + + // export Repository content to export package + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(context.getLocation()); + parameters.setCrawlSelf(context.self); + parameters.setCrawlChildNodes(context.children); + + try + { + exporter.exportView(exportHandler, parameters, new ExportProgress()); + } + catch(ExporterException e) + { + throw new ToolException("Failed to export", e); + } + } + + /** + * Handler for exporting Repository content streams to file system files + * + * @author David Caruana + */ + private class FileHandler extends FileExportPackageHandler + { + /** + * Construct + * + * @param destDir + * @param dataFile + * @param contentDir + * @param overwrite + */ + public FileHandler(File destDir, File dataFile, File contentDir, boolean overwrite) + { + super(destDir, dataFile, contentDir, overwrite); + } + + /** + * Log Export Message + * + * @param message message to log + */ + protected void log(String message) + { + Export.this.log(message); + } + } + + /** + * Handler for exporting Repository content streams to zip file + * + * @author David Caruana + */ + private class ZipHandler extends ACPExportPackageHandler + { + /** + * Construct + * + * @param destDir + * @param zipFile + * @param dataFile + * @param contentDir + */ + public ZipHandler(File destDir, File zipFile, File dataFile, File contentDir, boolean overwrite) + { + super(destDir, zipFile, dataFile, contentDir, overwrite); + } + + /** + * Log Export Message + * + * @param message message to log + */ + protected void log(String message) + { + Export.this.log(message); + } + } + + /** + * Export Tool Context + * + * @author David Caruana + */ + private class ExportContext extends ToolContext + { + /** Store Reference to export from */ + private StoreRef storeRef; + /** Path to export from */ + private String path; + /** Destination directory to export to */ + private String destDir; + /** The package directory within the destination directory to export to */ + private String packageDir; + /** The package name to export to */ + private String packageName; + /** Export children */ + private boolean children = true; + /** Export self */ + private boolean self = false; + /** Force overwrite of existing package */ + private boolean overwrite = false; + /** Zipped? */ + private boolean zipped = false; + + /* (non-Javadoc) + * @see org.alfresco.tools.ToolContext#validate() + */ + @Override + /*package*/ void validate() + { + super.validate(); + + if (storeRef == null) + { + throw new ToolException("Store to export from has not been specified."); + } + if (packageName == null) + { + throw new ToolException("Package name has not been specified."); + } + if (destDir != null) + { + File fileDestDir = new File(destDir); + if (fileDestDir.exists() == false) + { + throw new ToolException("Destination directory " + fileDestDir.getAbsolutePath() + " does not exist."); + } + } + } + + /** + * Get the location within the Repository to export from + * + * @return the location + */ + private Location getLocation() + { + Location location = new Location(storeRef); + location.setPath(path); + return location; + } + + /** + * Get the destination directory + * + * @return the destination directory (or null if current directory) + */ + private File getDestDir() + { + File dir = (destDir == null) ? null : new File(destDir); + return dir; + } + + /** + * Get the package directory + * + * @return the package directory within the destination directory + */ + private File getPackageDir() + { + File dir = null; + if (packageDir != null) + { + dir = new File(packageDir); + } + else if (packageName.indexOf('.') != -1) + { + dir = new File(packageName.substring(0, packageName.indexOf('.'))); + } + else + { + dir = new File(packageName); + } + return dir; + } + + /** + * Get the xml export file + * + * @return the package file + */ + private File getPackageFile() + { + String packageFile = (packageName.indexOf('.') != -1) ? packageName : packageName + ".xml"; + File file = new File(packageFile); + return file; + } + + /** + * Get the zip file + * + * @return the zip file + */ + private File getZipFile() + { + int iExt = packageName.indexOf('.'); + String zipFile = ((iExt != -1) ? packageName.substring(0, iExt) : packageName) + ".acp"; + return new File(zipFile); + } + } + + + /** + * Report Export Progress + * + * @author David Caruana + */ + private class ExportProgress + implements Exporter + { + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#start() + */ + public void start(ExporterContext exportNodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNamespace(java.lang.String, java.lang.String) + */ + public void startNamespace(String prefix, String uri) + { + logVerbose("Exporting namespace " + uri + " (prefix: " + prefix + ")"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNamespace(java.lang.String) + */ + public void endNamespace(String prefix) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startNode(NodeRef nodeRef) + { + logVerbose("Exporting node " + nodeRef.toString()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endNode(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endNode(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAspects(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspects(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAspects(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAspect(NodeRef nodeRef, QName aspect) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAspect(NodeRef nodeRef, QName aspect) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startProperties(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperties(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endProperties(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startProperty(NodeRef nodeRef, QName property) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endProperty(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endProperty(NodeRef nodeRef, QName property) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable) + */ + public void value(NodeRef nodeRef, QName property, Object value) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Collection) + */ + public void value(NodeRef nodeRef, QName property, Collection values) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#content(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.InputStream) + */ + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void startAssoc(NodeRef nodeRef, QName assoc) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssoc(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void endAssoc(NodeRef nodeRef, QName assoc) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#startAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void startAssocs(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#endAssocs(org.alfresco.service.cmr.repository.NodeRef) + */ + public void endAssocs(NodeRef nodeRef) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#warning(java.lang.String) + */ + public void warning(String warning) + { + log("Warning: " + warning); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.Exporter#end() + */ + public void end() + { + } + } + +} diff --git a/source/java/org/alfresco/tools/Import.java b/source/java/org/alfresco/tools/Import.java new file mode 100644 index 0000000000..a226acb684 --- /dev/null +++ b/source/java/org/alfresco/tools/Import.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.tools; + +import java.io.File; +import java.io.Serializable; +import java.nio.charset.Charset; + +import org.alfresco.repo.importer.ACPImportPackageHandler; +import org.alfresco.repo.importer.FileImportPackageHandler; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ImportPackageHandler; +import org.alfresco.service.cmr.view.ImporterException; +import org.alfresco.service.cmr.view.ImporterProgress; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; + + +/** + * Import Tool. + * + * @author David Caruana + */ +public class Import extends Tool +{ + /** Import Tool Context */ + private ImportContext context; + + + /** + * Entry Point + * + * @param args + */ + public static void main(String[] args) + { + Tool tool = new Import(); + tool.start(args); + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#processArgs(java.lang.String[]) + */ + @Override + /*package*/ ToolContext processArgs(String[] args) + { + context = new ImportContext(); + context.setLogin(true); + + int i = 0; + while (i < args.length) + { + if (args[i].equals("-h") || args[i].equals("-help")) + { + context.setHelp(true); + break; + } + else if (args[i].equals("-s") || args[i].equals("-store")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -store must be specified"); + } + context.storeRef = new StoreRef(args[i]); + } + else if (args[i].equals("-p") || args[i].equals("-path")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -path must be specified"); + } + context.path = args[i]; + } + else if (args[i].equals("-d") || args[i].equals("-dir")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -dir must be specified"); + } + context.sourceDir = args[i]; + } + else if (args[i].equals("-user")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -user must be specified"); + } + context.setUsername(args[i]); + } + else if (args[i].equals("-pwd")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -pwd must be specified"); + } + context.setPassword(args[i]); + } + else if (args[i].equals("-encoding")) + { + i++; + if (i == args.length || args[i].length() == 0) + { + throw new ToolException("The value for the option -encoding must be specified"); + } + context.encoding = args[i]; + } + else if (args[i].equals("-quiet")) + { + context.setQuiet(true); + } + else if (args[i].equals("-verbose")) + { + context.setVerbose(true); + } + else if (i == (args.length - 1)) + { + context.packageName = args[i]; + } + else + { + throw new ToolException("Unknown option " + args[i]); + } + + // next argument + i++; + } + + return context; + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#displayHelp() + */ + @Override + /*package*/ void displayHelp() + { + System.out.println("Usage: import -user username -s[tore] store [options] packagename"); + System.out.println(""); + System.out.println("username: username for login"); + System.out.println("store: the store to import into the form of scheme://store_name"); + System.out.println("packagename: the filename to import from (with or without extension)"); + System.out.println(""); + System.out.println("Options:"); + System.out.println(" -h[elp] display this help"); + System.out.println(" -p[ath] the path within the store to extract into (default: /)"); + System.out.println(" -d[ir] the source directory to import from (default: current directory)"); + System.out.println(" -pwd password for login"); + System.out.println(" -encoding package file encoding (default: " + Charset.defaultCharset() + ")"); + System.out.println(" -quiet do not display any messages during import"); + System.out.println(" -verbose report import progress"); + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#getToolName() + */ + @Override + /*package*/ String getToolName() + { + return "Alfresco Repository Importer"; + } + + /* (non-Javadoc) + * @see org.alfresco.tools.Tool#execute() + */ + @Override + /*package*/ void execute() throws ToolException + { + ImporterService importer = getServiceRegistry().getImporterService(); + + // determine type of import (from zip or file system) + ImportPackageHandler importHandler; + if (context.zipFile) + { + importHandler = new ZipHandler(context.getSourceDir(), context.getPackageFile(), context.encoding); + } + else + { + importHandler = new FileHandler(context.getSourceDir(), context.getPackageFile(), context.encoding); + } + + try + { + importer.importView(importHandler, context.getLocation(), null, new ImportProgress()); + } + catch(ImporterException e) + { + throw new ToolException("Failed to import package due to " + e.getMessage(), e); + } + } + + /** + * Handler for importing Repository content from zip package + * + * @author David Caruana + */ + private class ZipHandler extends ACPImportPackageHandler + { + /** + * Construct + * + * @param sourceDir + * @param dataFile + * @param dataFileEncoding + */ + public ZipHandler(File sourceDir, File dataFile, String dataFileEncoding) + { + super(new File(sourceDir, dataFile.getPath()), dataFileEncoding); + } + + /** + * Log Export Message + * + * @param message message to log + */ + protected void log(String message) + { + Import.this.log(message); + } + } + + /** + * Handler for importing Repository content from file system files + * + * @author David Caruana + */ + private class FileHandler extends FileImportPackageHandler + { + /** + * Construct + * + * @param sourceDir + * @param dataFile + * @param dataFileEncoding + */ + public FileHandler(File sourceDir, File dataFile, String dataFileEncoding) + { + super(sourceDir, dataFile, dataFileEncoding); + } + + /** + * Log Export Message + * + * @param message message to log + */ + protected void log(String message) + { + Import.this.log(message); + } + } + + /** + * Report Import Progress + * + * @author David Caruana + */ + private class ImportProgress + implements ImporterProgress + { + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterProgress#nodeCreated(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName) + */ + public void nodeCreated(NodeRef nodeRef, NodeRef parentRef, QName assocName, QName childName) + { + logVerbose("Imported node " + nodeRef + " (parent=" + parentRef + ", childname=" + childName + ", association=" + assocName + ")"); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterProgress#contentCreated(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public void contentCreated(NodeRef nodeRef, String sourceUrl) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterProgress#propertySet(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable) + */ + public void propertySet(NodeRef nodeRef, QName property, Serializable value) + { + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.view.ImporterProgress#aspectAdded(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void aspectAdded(NodeRef nodeRef, QName aspect) + { + } + } + + /** + * Import Tool Context + * + * @author David Caruana + */ + private class ImportContext extends ToolContext + { + /** Store Reference to import into */ + private StoreRef storeRef; + /** Path to import into */ + private String path; + /** Source directory to import from */ + private String sourceDir; + /** The package name to import */ + private String packageName; + /** The package encoding */ + private String encoding = null; + /** Zip Package? */ + private boolean zipFile = false; + + /* (non-Javadoc) + * @see org.alfresco.tools.ToolContext#validate() + */ + @Override + /*package*/ void validate() + { + super.validate(); + + if (storeRef == null) + { + throw new ToolException("Store to import into has not been specified."); + } + if (packageName == null) + { + throw new ToolException("Package name has not been specified."); + } + if (sourceDir != null) + { + File fileSourceDir = getSourceDir(); + if (fileSourceDir.exists() == false) + { + throw new ToolException("Source directory " + fileSourceDir.getAbsolutePath() + " does not exist."); + } + } + if (packageName.endsWith(".acp")) + { + File packageFile = new File(getSourceDir(), packageName); + if (!packageFile.exists()) + { + throw new ToolException("Package zip file " + packageFile.getAbsolutePath() + " does not exist."); + } + zipFile = true; + } + else + { + File packageFile = new File(getSourceDir(), getDataFile().getPath()); + if (!packageFile.exists()) + { + throw new ToolException("Package file " + packageFile.getAbsolutePath() + " does not exist."); + } + } + } + + /** + * Get the location within the Repository to import into + * + * @return the location + */ + private Location getLocation() + { + Location location = new Location(storeRef); + location.setPath(path); + return location; + } + + /** + * Get the source directory + * + * @return the source directory (or null if current directory) + */ + private File getSourceDir() + { + File dir = (sourceDir == null) ? null : new File(sourceDir); + return dir; + } + + /** + * Get the xml import file + * + * @return the package file + */ + private File getDataFile() + { + String dataFile = (packageName.indexOf('.') != -1) ? packageName : packageName + ".xml"; + File file = new File(dataFile); + return file; + } + + /** + * Get the zip import file (.acp - alfresco content package) + * + * @return the zip package file + */ + private File getPackageFile() + { + return (zipFile) ? new File(packageName) : getDataFile(); + } + } + +} diff --git a/source/java/org/alfresco/tools/Tool.java b/source/java/org/alfresco/tools/Tool.java new file mode 100644 index 0000000000..7aec93d8b3 --- /dev/null +++ b/source/java/org/alfresco/tools/Tool.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.tools; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + + +/** + * Abstract Tool Implementation + * + * @author David Caruana + */ +public abstract class Tool +{ + /** Tool Context */ + private ToolContext toolContext; + /** Spring Application Context */ + private ApplicationContext appContext; + /** Repository Service Registry */ + private ServiceRegistry serviceRegistry; + + + /** + * Process Tool Arguments + * + * @param args the arguments + * @return the tool context + * @throws ToolException + */ + /*package*/ ToolContext processArgs(String[] args) + throws ToolException + { + return new ToolContext(); + } + + /** + * Display Tool Help + */ + /*package*/ void displayHelp() + { + System.out.println("Sorry. Help is not available."); + } + + /** + * Perform Tool Behaviour + * + * @throws ToolException + */ + /*package*/ abstract void execute() + throws ToolException; + + /** + * Get the tool name + * + * @return the tool name + */ + /*package*/ abstract String getToolName(); + + /** + * Get the Application Context + * + * @return the application context + */ + /*package*/ final ApplicationContext getApplicationContext() + { + return appContext; + } + + /** + * Get the Repository Service Registry + * + * @return the service registry + */ + /*package*/ final ServiceRegistry getServiceRegistry() + { + return serviceRegistry; + } + + /** + * Log message + * + * @param msg message to log + */ + /*package*/ final void log(String msg) + { + if (toolContext.isQuiet() == false) + { + System.out.println(msg); + } + } + + /** + * Log Verbose message + * + * @param msg message to log + */ + /*package*/ final void logVerbose(String msg) + { + if (toolContext.isVerbose()) + { + log(msg); + } + } + + /** + * Tool entry point + * + * @param args the tool arguments + */ + /*package*/ final void start(String[] args) + { + try + { + // Process tool arguments + toolContext = processArgs(args); + toolContext.validate(); + + try + { + if (toolContext.isHelp()) + { + // Display help, if requested + displayHelp(); + } + else + { + // Perform Tool behaviour + log(getToolName()); + initialiseRepository(); + login(); + execute(); + log(getToolName() + " successfully completed."); + } + System.exit(0); + } + catch (ToolException e) + { + displayError(e); + System.exit(-1); + } + } + catch(ToolException e) + { + System.out.println(e.getMessage()); + System.out.println(); + displayHelp(); + System.exit(-1); + } + catch (Throwable e) + { + System.out.println("The following error has occurred:"); + System.out.println(e.getMessage()); + e.printStackTrace(); + System.exit(-1); + } + } + + /** + * Login to Repository + */ + private void login() + { + // TODO: Replace with call to ServiceRegistry + AuthenticationService auth = (AuthenticationService)appContext.getBean("authenticationService"); + auth.authenticate(toolContext.getUsername(), toolContext.getPassword().toCharArray()); + log("Connected as " + toolContext.getUsername()); + } + + /** + * Initialise the Repository + */ + private void initialiseRepository() + { + appContext = ApplicationContextHelper.getApplicationContext(); + serviceRegistry = (ServiceRegistry) appContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + } + + /** + * Display Error Message + * + * @param e exception + */ + private void displayError(Throwable e) + { + System.out.println(e.getMessage()); + if (toolContext != null && toolContext.isVerbose()) + { + e.printStackTrace(); + } + } + +} diff --git a/source/java/org/alfresco/tools/ToolContext.java b/source/java/org/alfresco/tools/ToolContext.java new file mode 100644 index 0000000000..eba38883d9 --- /dev/null +++ b/source/java/org/alfresco/tools/ToolContext.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.tools; + + +/** + * Tool Context + * + * @author David Caruana + */ +/*package*/ class ToolContext +{ + /** Help required? */ + private boolean help = false; + /** Login required? */ + private boolean login = false; + /** Username */ + private String username = null; + /** Password */ + private String password = ""; + /** Log messages whilst importing? */ + private boolean quiet = false; + /** Verbose logging */ + private boolean verbose = false; + + + /** + * Is help required? + * + * @return true => help is required + */ + /*package*/ final boolean isHelp() + { + return help; + } + + /** + * Sets whether help is required + * + * @param help + */ + /*package*/ final void setHelp(boolean help) + { + this.help = help; + } + + /** + * Is login required? + * + * @return true => login is required + */ + /*package*/ final boolean isLogin() + { + return login; + } + + /** + * Sets whether login is required + * + * @param login + */ + /*package*/ final void setLogin(boolean login) + { + this.login = login; + } + + /** + * Get the password + * + * @return the password + */ + /*package*/ final String getPassword() + { + return password; + } + + /** + * Set the password + * + * @param password + */ + /*package*/ final void setPassword(String password) + { + this.password = password; + } + + /** + * Is output is required? + * + * @return true => output is required + */ + /*package*/ final boolean isQuiet() + { + return quiet; + } + + /** + * Sets whether output is required + * + * @param quiet + */ + /*package*/ final void setQuiet(boolean quiet) + { + this.quiet = quiet; + } + + /** + * Get the username + * + * @return the username + */ + /*package*/ final String getUsername() + { + return username; + } + + /** + * Set the username + * + * @param username + */ + /*package*/ final void setUsername(String username) + { + this.username = username; + } + + /** + * Is verbose logging required? + * + * @return true => verbose logging is required + */ + /*package*/ final boolean isVerbose() + { + return verbose; + } + + /** + * Sets whether verbose logging is required + * + * @param verbose + */ + /*package*/ final void setVerbose(boolean verbose) + { + this.verbose = verbose; + } + + /** + * Validate Tool Context + */ + /*package*/ void validate() + throws ToolException + { + if (login) + { + if (username == null || username.length() == 0) + { + throw new ToolException("Username for login has not been specified."); + } + } + } + +} diff --git a/source/java/org/alfresco/tools/ToolException.java b/source/java/org/alfresco/tools/ToolException.java new file mode 100644 index 0000000000..a735abfdc0 --- /dev/null +++ b/source/java/org/alfresco/tools/ToolException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.tools; + +/** + * Tool Exception + * + * @author David Caruana + */ +/*package*/ class ToolException extends RuntimeException +{ + private static final long serialVersionUID = 3257008761007847733L; + + /*package*/ ToolException(String msg) + { + super(msg); + } + + /*package*/ ToolException(String msg, Throwable cause) + { + super(msg, cause); + } + +} diff --git a/source/java/org/alfresco/util/ApplicationContextHelper.java b/source/java/org/alfresco/util/ApplicationContextHelper.java new file mode 100644 index 0000000000..5916d7af4c --- /dev/null +++ b/source/java/org/alfresco/util/ApplicationContextHelper.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * Helper class to provide static and common access to the Spring + * {@link org.springframework.context.ApplicationContext application context}. + * + * @author Derek Hulley + */ +public class ApplicationContextHelper +{ + /** location of required configuration files */ + public static final String[] CONFIG_LOCATIONS = new String[] { "classpath:alfresco/application-context.xml" }; + + /** + * Instantiates a new application context. + * + * @return Returns a new application context + */ + public static ApplicationContext getApplicationContext() + { + return new ClassPathXmlApplicationContext(CONFIG_LOCATIONS); + } +} diff --git a/source/java/org/alfresco/util/BaseAlfrescoSpringTest.java b/source/java/org/alfresco/util/BaseAlfrescoSpringTest.java new file mode 100644 index 0000000000..db1a902398 --- /dev/null +++ b/source/java/org/alfresco/util/BaseAlfrescoSpringTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.transaction.TransactionService; + +/** + * Base Alfresco test. + * + * Creates a store and root node that can be used in the tests. + * + * Runs all tests as the system user. + * + * @author Roy Wetherall + */ +public abstract class BaseAlfrescoSpringTest extends BaseSpringTest +{ + /** The node service */ + protected NodeService nodeService; + + /** The content service */ + protected ContentService contentService; + + /** The authentication service */ + protected AuthenticationService authenticationService; + + /** The store reference */ + protected StoreRef storeRef; + + /** The root node reference */ + protected NodeRef rootNodeRef; + + + protected ActionService actionService; + protected TransactionService transactionService; + + /** + * On setup in transaction override + */ + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get a reference to the node service + this.nodeService = (NodeService) this.applicationContext.getBean("nodeService"); + this.contentService = (ContentService) this.applicationContext.getBean("contentService"); + this.authenticationService = (AuthenticationService) this.applicationContext.getBean("authenticationService"); + this.actionService = (ActionService)this.applicationContext.getBean("actionService"); + this.transactionService = (TransactionService)this.applicationContext.getBean("transactionComponent"); + + // Authenticate as the system user + AuthenticationComponent authenticationComponent = (AuthenticationComponent) this.applicationContext + .getBean("authenticationComponent"); + authenticationComponent.setSystemUserAsCurrentUser(); + + // Create the store and get the root node + this.storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.storeRef); + + + + } + + @Override + protected void onTearDownInTransaction() + { + authenticationService.clearCurrentSecurityContext(); + super.onTearDownInTransaction(); + } + +} diff --git a/source/java/org/alfresco/util/BaseAlfrescoTestCase.java b/source/java/org/alfresco/util/BaseAlfrescoTestCase.java new file mode 100644 index 0000000000..250e673de8 --- /dev/null +++ b/source/java/org/alfresco/util/BaseAlfrescoTestCase.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + + +import junit.framework.TestCase; + +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.transaction.TransactionService; +import org.springframework.context.ApplicationContext; + +/** + * Base Alfresco test. + * + * Creates a store and root node that can be used in the tests. + * + * @author Roy Wetherall + */ +public abstract class BaseAlfrescoTestCase extends TestCase +{ + /** the context to keep between tests */ + public static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + /** the service registry */ + protected ServiceRegistry serviceRegistry; + + /** The node service */ + protected NodeService nodeService; + + /** The content service */ + protected ContentService contentService; + + /** The authentication service */ + protected AuthenticationService authenticationService; + + /** The store reference */ + protected StoreRef storeRef; + + /** The root node reference */ + protected NodeRef rootNodeRef; + + + protected ActionService actionService; + protected TransactionService transactionService; + + + @Override + protected void setUp() throws Exception + { + super.setUp(); + // get the service register + this.serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + //Get a reference to the node service + this.nodeService = (NodeService)ctx.getBean("NodeService"); + this.contentService = (ContentService)ctx.getBean("ContentService"); + this.authenticationService = (AuthenticationService)ctx.getBean("authenticationService"); + this.actionService = (ActionService)ctx.getBean("actionService"); + this.transactionService = (TransactionService)ctx.getBean("transactionComponent"); + + // Authenticate as the system user - this must be done before we create the store + AuthenticationComponent authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + authenticationComponent.setSystemUserAsCurrentUser(); + + // Create the store and get the root node + this.storeRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + this.rootNodeRef = this.nodeService.getRootNode(this.storeRef); + + + } + + + @Override + protected void tearDown() throws Exception + { + authenticationService.clearCurrentSecurityContext(); + super.tearDown(); + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/util/BaseSpringTest.java b/source/java/org/alfresco/util/BaseSpringTest.java new file mode 100644 index 0000000000..965fe16f82 --- /dev/null +++ b/source/java/org/alfresco/util/BaseSpringTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.orm.hibernate3.SessionFactoryUtils; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +/** + * Base test class providing Hibernate sessions. + *

    + * By default this is auto-wired by type. If a this is going to + * result in a conlict the use auto-wire by name. This can be done by + * setting populateProtectedVariables to true in the constructor and + * then adding protected members with the same name as the bean you require. + * + * @author Derek Hulley + */ +public abstract class BaseSpringTest extends AbstractTransactionalDataSourceSpringContextTests +{ + /** protected so that it gets populated if autowiring is done by variable name **/ + protected SessionFactory sessionFactory; + + /** + * Constructor + */ + public BaseSpringTest() + { + } + + /** + * Setter present for in case autowiring is done by type + * + * @param sessionFactory + */ + public void setSessionFactory(SessionFactory sessionFactory) + { + this.sessionFactory = sessionFactory; + } + + /** + * @return Returns the existing session attached to the thread. + * A new session will not be created. + */ + protected Session getSession() + { + return SessionFactoryUtils.getSession(sessionFactory, true); + } + + /** + * Forces the session to flush to the database (without commiting) and clear the + * cache. This ensures that all reads against the session are fresh instances, + * which gives the assurance that the DB read/write operations occur correctly. + */ + protected void flushAndClear() + { + getSession().flush(); + getSession().clear(); + } + + /** + * Get the config locations + * + * @return an array containing the config locations + */ + protected String[] getConfigLocations() + { + if (logger.isDebugEnabled()) + { + logger.debug("Getting config locations"); + } + return ApplicationContextHelper.CONFIG_LOCATIONS; + } +} diff --git a/source/java/org/alfresco/util/PropertyMap.java b/source/java/org/alfresco/util/PropertyMap.java new file mode 100644 index 0000000000..92d7ac406b --- /dev/null +++ b/source/java/org/alfresco/util/PropertyMap.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import java.io.Serializable; +import java.util.HashMap; + +import org.alfresco.service.namespace.QName; + +/** + * Property map helper class. + *

    + * This class can be used as a short hand when a class of type + * Map is required. + * + * @author Roy Wetherall + */ +public class PropertyMap extends HashMap +{ + private static final long serialVersionUID = 8052326301073209645L; + + /** + * @see HashMap#HashMap(int, float) + */ + public PropertyMap(int initialCapacity, float loadFactor) + { + super(initialCapacity, loadFactor); + } + + /** + * @see HashMap#HashMap(int) + */ + public PropertyMap(int initialCapacity) + { + super(initialCapacity); + } + + /** + * @see HashMap#HashMap() + */ + public PropertyMap() + { + super(); + } +} diff --git a/source/java/org/alfresco/util/SearchLanguageConversion.java b/source/java/org/alfresco/util/SearchLanguageConversion.java new file mode 100644 index 0000000000..65efcdd584 --- /dev/null +++ b/source/java/org/alfresco/util/SearchLanguageConversion.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import org.alfresco.repo.search.impl.lucene.QueryParser; + + +/** + * Helper class to provide conversions between different search languages + * @author Derek Hulley + */ +public class SearchLanguageConversion +{ + /** + * XPath like query language summary: + *

      + *
    • Escape: \
    • + *
    • Single char search: _
    • + *
    • Multiple char search: %
    • + *
    • Reserved: \%_
    • + *
    + */ + public static LanguageDefinition DEF_XPATH_LIKE = new SimpleLanguageDef('\\', "%", "_", "\\%_"); + /** + * Regular expression query language summary: + *
      + *
    • Escape: \
    • + *
    • Single char search: .
    • + *
    • Multiple char search: .*
    • + *
    • Reserved: \*.+?^$(){}|
    • + *
    + */ + public static LanguageDefinition DEF_REGEX = new SimpleLanguageDef('\\', ".*", ".", "\\*.+?^$(){}|"); + /** + * Lucene syntax summary: {@link QueryParser#escape(String) Lucene Query Parser} + */ + public static LanguageDefinition DEF_LUCENE = new LuceneLanguageDef(); + + /** + * Escape a string according to the XPath like function syntax. + * + * @param str the string to escape + * @return Returns the escaped string + */ + public static String escapeForXPathLike(String str) + { + return escape(DEF_XPATH_LIKE, str); + } + + /** + * Escape a string according to the regex language syntax. + * + * @param str the string to escape + * @return Returns the escaped string + */ + public static String escapeForRegex(String str) + { + return escape(DEF_REGEX, str); + } + + /** + * Escape a string according to the Lucene query syntax. + * + * @param str the string to escape + * @return Returns the escaped string + */ + public static String escapeForLucene(String str) + { + return escape(DEF_LUCENE, str); + } + + /** + * Generic escaping using the language definition + */ + private static String escape(LanguageDefinition def, String str) + { + StringBuilder sb = new StringBuilder(str.length() * 2); + + char[] chars = str.toCharArray(); + for (int i = 0; i < chars.length; i++) + { + // first check for reserved chars + if (def.isReserved(chars[i])) + { + // escape it + sb.append(def.escapeChar); + } + sb.append(chars[i]); + } + return sb.toString(); + } + + /** + * Convert an xpath like function clause into a regex query. + * + * @param xpathLikeClause + * @return Returns a valid regular expression that is equivalent to the + * given xpath like clause. + */ + public static String convertXPathLikeToRegex(String xpathLikeClause) + { + return convert(DEF_XPATH_LIKE, DEF_REGEX, xpathLikeClause); + } + + /** + * Convert an xpath like function clause into a Lucene query. + * + * @param xpathLikeClause + * @return Returns a valid Lucene expression that is equivalent to the + * given xpath like clause. + */ + public static String convertXPathLikeToLucene(String xpathLikeClause) + { + return convert(DEF_XPATH_LIKE, DEF_LUCENE, xpathLikeClause); + } + + public static String convert(LanguageDefinition from, LanguageDefinition to, String query) + { + char[] chars = query.toCharArray(); + + StringBuilder sb = new StringBuilder(chars.length * 2); + + boolean escaping = false; + + for (int i = 0; i < chars.length; i++) + { + if (escaping) // if we are currently escaping, just escape the current character + { + sb.append(to.escapeChar); // the to format escape char + sb.append(chars[i]); // the current char + escaping = false; + } + else if (chars[i] == from.escapeChar) // not escaping and have escape char + { + escaping = true; + } + else if (query.startsWith(from.multiCharWildcard, i)) // not escaping but have multi-char wildcard + { + // translate the wildcard + sb.append(to.multiCharWildcard); + } + else if (query.startsWith(from.singleCharWildcard, i)) // have single-char wildcard + { + // translate the wildcard + sb.append(to.singleCharWildcard); + } + else if (to.isReserved(chars[i])) // reserved character + { + sb.append(to.escapeChar).append(chars[i]); + } + else // just a normal char in both + { + sb.append(chars[i]); + } + } + return sb.toString(); + } + + /** + * Simple store of special characters for a given query language + */ + public static abstract class LanguageDefinition + { + public final char escapeChar; + public final String multiCharWildcard; + public final String singleCharWildcard; + + public LanguageDefinition(char escapeChar, String multiCharWildcard, String singleCharWildcard) + { + this.escapeChar = escapeChar; + this.multiCharWildcard = multiCharWildcard; + this.singleCharWildcard = singleCharWildcard; + } + public abstract boolean isReserved(char ch); + } + private static class SimpleLanguageDef extends LanguageDefinition + { + private String reserved; + public SimpleLanguageDef(char escapeChar, String multiCharWildcard, String singleCharWildcard, String reserved) + { + super(escapeChar, multiCharWildcard, singleCharWildcard); + this.reserved = reserved; + } + @Override + public boolean isReserved(char ch) + { + return (reserved.indexOf(ch) > -1); + } + } + private static class LuceneLanguageDef extends LanguageDefinition + { + private String reserved; + public LuceneLanguageDef() + { + super('\\', "*", "?"); + init(); + } + /** + * Discovers all the reserved chars + */ + private void init() + { + StringBuilder sb = new StringBuilder(20); + for (char ch = 0; ch < 256; ch++) + { + char[] chars = new char[] {ch}; + String unescaped = new String(chars); + // check it + String escaped = QueryParser.escape(unescaped); + if (!escaped.equals(unescaped)) + { + // it was escaped + sb.append(ch); + } + } + reserved = sb.toString(); + } + @Override + public boolean isReserved(char ch) + { + return (reserved.indexOf(ch) > -1); + } + } +} diff --git a/source/java/org/alfresco/util/SearchLanguageConversionTest.java b/source/java/org/alfresco/util/SearchLanguageConversionTest.java new file mode 100644 index 0000000000..47b1dcfa7d --- /dev/null +++ b/source/java/org/alfresco/util/SearchLanguageConversionTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.util.SearchLanguageConversion + * + * @author Derek Hulley + */ +public class SearchLanguageConversionTest extends TestCase +{ + /** + * A string with a whole lod of badness to stress test with + */ + private static final String BAD_STRING = + "\\ | ! \" £ " + + "$ % ^ & * ( " + + ") _ { } [ ] " + + "@ # ~ ' : ; " + + ", . < > + ? " + + "/ \\\\ \\* \\? \\_"; + + public void testEscapeXPathLike() + { + String good = SearchLanguageConversion.escapeForXPathLike(BAD_STRING); + assertEquals("Escaping for xpath failed", + "\\\\ | ! \" £ " + + "$ \\% ^ & * ( " + + ") \\_ { } [ ] " + + "@ # ~ ' : ; " + + ", . < > + ? " + + "/ \\\\\\\\ \\\\* \\\\? \\\\\\_", + good); + } + + public void testEscapeRegex() + { + String good = SearchLanguageConversion.escapeForRegex(BAD_STRING); + assertEquals("Escaping for regex failed", + "\\\\ \\| ! \" £ " + + "\\$ % \\^ & \\* \\( " + + "\\) _ \\{ \\} [ ] " + + "@ # ~ ' : ; " + + ", \\. < > \\+ \\? " + + "/ \\\\\\\\ \\\\\\* \\\\\\? \\\\_", + good); + } + + public void testEscapeLucene() + { + String good = SearchLanguageConversion.escapeForLucene(BAD_STRING); + assertEquals("Escaping for regex failed", + "\\\\ | \\! \\\" £ " + + "$ % \\^ & \\* \\( " + + "\\) _ \\{ \\} \\[ \\] " + + "@ # \\~ ' \\: ; " + + ", . < > \\+ \\? " + + "/ \\\\\\\\ \\\\\\* \\\\\\? \\\\_", + good); + } + + public void testConvertXPathLikeToRegex() + { + String good = SearchLanguageConversion.convertXPathLikeToRegex(BAD_STRING); + assertEquals("XPath like to regex failed", + "\\ \\| ! \" £ " + + "\\$ .* \\^ & \\* \\( " + + "\\) . \\{ \\} [ ] " + + "@ # ~ ' : ; " + + ", \\. < > \\+ \\? " + + "/ \\\\ \\* \\? \\_", + good); + } + + public void testConvertXPathLikeToLucene() + { + String good = SearchLanguageConversion.convertXPathLikeToLucene(BAD_STRING); + assertEquals("XPath like to regex failed", + "\\ | \\! \\\" £ " + + "$ * \\^ & \\* \\( " + + "\\) ? \\{ \\} \\[ \\] " + + "@ # \\~ ' \\: ; " + + ", . < > \\+ \\? " + + "/ \\\\ \\* \\? \\_", + good); + } +} diff --git a/source/java/org/alfresco/util/TestWithUserUtils.java b/source/java/org/alfresco/util/TestWithUserUtils.java new file mode 100644 index 0000000000..89d973a8be --- /dev/null +++ b/source/java/org/alfresco/util/TestWithUserUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import java.io.Serializable; +import java.util.HashMap; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Utility class containing some useful methods to help when writing tets that require authenticated users + * + * @author Roy Wetherall + */ +public abstract class TestWithUserUtils extends BaseSpringTest +{ + /** + * Create a new user, including the corresponding person node. + * + * @param userName the user name + * @param password the password + * @param rootNodeRef the root node reference + * @param nodeService the node service + * @param authenticationService the authentication service + */ + public static void createUser( + String userName, + String password, + NodeRef rootNodeRef, + NodeService nodeService, + AuthenticationService authenticationService) + { + QName children = ContentModel.ASSOC_CHILDREN; + QName system = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "system"); + QName container = ContentModel.TYPE_CONTAINER; + QName types = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "people"); + + NodeRef systemNodeRef = nodeService.createNode(rootNodeRef, children, system, container).getChildRef(); + NodeRef typesNodeRef = nodeService.createNode(systemNodeRef, children, types, container).getChildRef(); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + NodeRef goodUserPerson = nodeService.createNode(typesNodeRef, children, ContentModel.TYPE_PERSON, container, properties).getChildRef(); + + // Create the users + + authenticationService.createAuthentication(userName, password.toCharArray()); + } + + /** + * Autneticate the user with the specified password + * + * @param userName the user name + * @param password the password + * @param rootNodeRef the root node reference + * @param authenticationService the authentication service + */ + public static void authenticateUser( + String userName, + String password, + NodeRef rootNodeRef, + AuthenticationService authenticationService) + { + authenticationService.authenticate(userName, password.toCharArray()); + } + + /** + * Get the current user node reference + * + * @param authenticationService the authentication service + * @return the currenlty authenticated user's node reference + */ + public static String getCurrentUser(AuthenticationService authenticationService) + { + String un = authenticationService.getCurrentUserName(); + if (un != null) + { + return un; + } + else + { + throw new RuntimeException("The current user could not be retrieved."); + } + + } + + public static void deleteUser(String user_name, String pwd, NodeRef ref, NodeService service, AuthenticationService service2) + { + service2.deleteAuthentication(user_name); + } + +} diff --git a/source/java/org/alfresco/util/ThreadPoolExecutorFactoryBean.java b/source/java/org/alfresco/util/ThreadPoolExecutorFactoryBean.java new file mode 100644 index 0000000000..82b14bb2d7 --- /dev/null +++ b/source/java/org/alfresco/util/ThreadPoolExecutorFactoryBean.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * Factory for {@link java.util.concurrent.ThreadPoolExecutor} instances, + * which cannot easily be constructed using constructor injection. + *

    + * This factory provides the a singleton instance of the pool. + * + * @author Derek Hulley + */ +public class ThreadPoolExecutorFactoryBean implements FactoryBean, InitializingBean +{ + private int corePoolSize; + private int maximumPoolSize; + private int keepAliveTime; + private BlockingQueue workQueue; + private ThreadPoolExecutor instance; + + /** + * Constructor setting default properties: + *

      + *
    • corePoolSize: 5
    • + *
    • maximumPoolSize: 20
    • + *
    • keepAliveTime: 60s
    • + *
    • workQueue: {@link ArrayBlockingQueue}
    • + *
    + */ + public ThreadPoolExecutorFactoryBean() + { + corePoolSize = 5; + maximumPoolSize = 20; + keepAliveTime = 30; + } + + /** + * The number of threads to keep in the pool, even if idle. + * + * @param corePoolSize core thread count + */ + public void setCorePoolSize(int corePoolSize) + { + this.corePoolSize = corePoolSize; + } + + /** + * The maximum number of threads to keep in the pool + * + * @param maximumPoolSize the maximum number of threads in the pool + */ + public void setMaximumPoolSize(int maximumPoolSize) + { + this.maximumPoolSize = maximumPoolSize; + } + + /** + * The time (in seconds) to keep non-core idle threads in the pool + * + * @param keepAliveTime time to stay idle in seconds + */ + public void setKeepAliveTime(int keepAliveTime) + { + this.keepAliveTime = keepAliveTime; + } + + /** + * The optional queue instance to use + * + * @param workQueue optional queue implementation + */ + public void setWorkQueue(BlockingQueue workQueue) + { + this.workQueue = workQueue; + } + + public void afterPropertiesSet() throws Exception + { + if (workQueue == null) + { + workQueue = new ArrayBlockingQueue(corePoolSize); + } + // construct the instance + instance = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue); + } + + /** + * @return Returns true always. + */ + public boolean isSingleton() + { + return true; + } + + /** + * @return Returns the singleton {@link ThreadPoolExecutor instance}. + */ + public Object getObject() throws Exception + { + if (instance == null) + { + throw new AlfrescoRuntimeException("The ThreadPoolExecutor instance has not been created"); + } + return instance; + } + + /** + * @see ThreadPoolExecutor + */ + public Class getObjectType() + { + return ThreadPoolExecutor.class; + } +} diff --git a/source/java/org/alfresco/util/debug/MethodCallLogAdvice.java b/source/java/org/alfresco/util/debug/MethodCallLogAdvice.java new file mode 100644 index 0000000000..a80048bf3c --- /dev/null +++ b/source/java/org/alfresco/util/debug/MethodCallLogAdvice.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.debug; + +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Performs writing to DEBUG of incoming arguments and outgoing results for a method call.
    + * If the method invocation throws an exception, then the incoming arguments are + * logged to DEBUG as well.
    + * The implementation adds very little overhead to a normal method + * call by only building log messages when required. + *

    + * The logging is done against the logger retrieved using the names: + *

    + *

    + *      org.alfresco.util.debug.MethodCallLogAdvice
    + *         AND
    + *      targetClassName
    + *      targetClassName.methodName
    + *      targetClassName.methodName.exception
    + * 
    + *

    + * The following examples show how to control the log levels: + *

    + *

    + *      org.alfresco.util.debug.MethodCallLogAdvice=DEBUG   # activate method logging
    + *          AND
    + *      x.y.MyClass=DEBUG                           # log debug for all method calls on MyClass
    + *      x.y.MyClass.doSomething=DEBUG               # log debug for all doSomething method calls
    + *      x.y.MyClass.doSomething.exception=DEBUG     # only log debug for doSomething() upon exception
    + * 
    + *

    + * + * @author Derek Hulley + */ +public class MethodCallLogAdvice implements MethodInterceptor +{ + private static final Log logger = LogFactory.getLog(MethodCallLogAdvice.class); + + public Object invoke(MethodInvocation invocation) throws Throwable + { + if (logger.isDebugEnabled()) + { + return invokeWithLogging(invocation); + } + else + { + // no logging required + return invocation.proceed(); + } + } + + /** + * Only executes logging code if logging is required + */ + private Object invokeWithLogging(MethodInvocation invocation) throws Throwable + { + String methodName = invocation.getMethod().getName(); + String className = invocation.getMethod().getDeclaringClass().getName(); + + // execute as normal + try + { + Object ret = invocation.proceed(); + // logging + Log methodLogger = LogFactory.getLog(className + "." + methodName); + if (methodLogger.isDebugEnabled()) + { + // log success + StringBuffer sb = getInvocationInfo(className, methodName, invocation.getArguments()); + sb.append(" Result: ").append(ret); + methodLogger.debug(sb); + } + // done + return ret; + } + catch (Throwable e) + { + Log exceptionLogger = LogFactory.getLog(className + "." + methodName + ".exception"); + if (exceptionLogger.isDebugEnabled()) + { + StringBuffer sb = getInvocationInfo(className, methodName, invocation.getArguments()); + sb.append(" Failure: ").append(e.getClass().getName()).append(" - ").append(e.getMessage()); + exceptionLogger.debug(sb); + } + // rethrow + throw e; + } + } + + /** + * Return format: + *

    +     *      Method: className#methodName
    +     *         Argument: arg0
    +     *         Argument: arg1
    +     *         ...
    +     *         Argument: argN {newline}
    +     * 
    + * + * @param className + * @param methodName + * @param args + * @return Returns a StringBuffer containing the details of a method call + */ + private StringBuffer getInvocationInfo(String className, String methodName, Object[] args) + { + StringBuffer sb = new StringBuffer(250); + sb.append("\nMethod: ").append(className).append("#").append(methodName).append("\n"); + sb.append(" Transaction: ").append(AlfrescoTransactionSupport.getTransactionId()).append("\n"); + for (Object arg : args) + { + sb.append(" Argument: ").append(arg).append("\n"); + } + return sb; + } +} diff --git a/source/java/org/alfresco/util/debug/NodeStoreInspector.java b/source/java/org/alfresco/util/debug/NodeStoreInspector.java new file mode 100644 index 0000000000..fa8c4538c3 --- /dev/null +++ b/source/java/org/alfresco/util/debug/NodeStoreInspector.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.debug; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Map; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * Debug class that has methods to inspect the contents of a node store. + * + * @author Roy Wetherall + */ +public class NodeStoreInspector +{ + /** + * Dumps the contents of a store to a string. + * + * @param nodeService the node service + * @param storeRef the store reference + * @return string containing textual representation of the contents of the store + */ + public static String dumpNodeStore(NodeService nodeService, StoreRef storeRef) + { + StringBuilder builder = new StringBuilder(); + + if (nodeService.exists(storeRef) == true) + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + builder.append(outputNode(0, nodeService, rootNode)); + } + else + { + builder. + append("The store "). + append(storeRef.toString()). + append(" does not exist."); + } + + return builder.toString(); + } + + /** + * Output the node + * + * @param iIndent + * @param nodeService + * @param nodeRef + * @return + */ + private static String outputNode(int iIndent, NodeService nodeService, NodeRef nodeRef) + { + StringBuilder builder = new StringBuilder(); + + try + { + QName nodeType = nodeService.getType(nodeRef); + builder. + append(getIndent(iIndent)). + append("node: "). + append(nodeRef.getId()). + append(" ("). + append(nodeType.getLocalName()); + + Collection aspects = nodeService.getAspects(nodeRef); + for (QName aspect : aspects) + { + builder. + append(", "). + append(aspect.getLocalName()); + } + + builder.append(")\n"); + + Map props = nodeService.getProperties(nodeRef); + for (QName name : props.keySet()) + { + String valueAsString = "null"; + Serializable value = props.get(name); + if (value != null) + { + valueAsString = value.toString(); + } + + builder. + append(getIndent(iIndent+1)). + append("@"). + append(name.getLocalName()). + append(" = "). + append(valueAsString). + append("\n"); + + } + + Collection childAssocRefs = nodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + builder. + append(getIndent(iIndent+1)). + append("-> "). + append(childAssocRef.getQName().toString()). + append(" ("). + append(childAssocRef.getQName().toString()). + append(")\n"); + + builder.append(outputNode(iIndent+2, nodeService, childAssocRef.getChildRef())); + } + + Collection assocRefs = nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL); + for (AssociationRef assocRef : assocRefs) + { + builder. + append(getIndent(iIndent+1)). + append("-> associated to "). + append(assocRef.getTargetRef().getId()). + append("\n"); + } + } + catch (InvalidNodeRefException invalidNode) + { + invalidNode.printStackTrace(); + } + + return builder.toString(); + } + + /** + * Get the indent + * + * @param iIndent the indent value + * @return the indent string + */ + private static String getIndent(int iIndent) + { + StringBuilder builder = new StringBuilder(iIndent*3); + for (int i = 0; i < iIndent; i++) + { + builder.append(" "); + } + return builder.toString(); + } + +} diff --git a/source/java/org/alfresco/util/debug/OutputSpacesStoreSystemTest.java b/source/java/org/alfresco/util/debug/OutputSpacesStoreSystemTest.java new file mode 100644 index 0000000000..3c0cb089b8 --- /dev/null +++ b/source/java/org/alfresco/util/debug/OutputSpacesStoreSystemTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.debug; + +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.BaseSpringTest; + +/** + * @author Roy Wetherall + */ +public class OutputSpacesStoreSystemTest extends BaseSpringTest +{ + /** + * Dump the contents of the spaces store to standard out + */ + public void testDumpSpacesStore() + { + NodeService nodeService = (NodeService)this.applicationContext.getBean("nodeService"); + StoreRef spacesStore = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, spacesStore)); + } +} diff --git a/source/java/org/alfresco/util/perf/AbstractPerformanceMonitor.java b/source/java/org/alfresco/util/perf/AbstractPerformanceMonitor.java new file mode 100644 index 0000000000..62cad7e053 --- /dev/null +++ b/source/java/org/alfresco/util/perf/AbstractPerformanceMonitor.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.perf; + +import java.text.DecimalFormat; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * An instance of this class keeps track of timings of method calls made against + * a named entity. Logging can occur either after each recorded time, or only on + * VM shutdown or both. + *

    + * Logging output is managed down to either the entity or entity-invocation level as follows: + *

    + *

    + *      performance.summary.method
    + *      performance.summary.vm
    + *          AND
    + *      performance.targetEntityName
    + *      performance.targetEntityName.methodName
    + * 
    + *

    + * The following examples illustrate how it can be used: + *

    + *

    + *      performance.summary.method=DEBUG
    + *      performance.myBean=DEBUG
    + *          --> Output method invocation statistic on each call to myBean
    + *          
    + *      performance.summary.vm=DEBUG
    + *      performance.myBean.doSomething=DEBUG
    + *          --> Output summary for doSomething() invocations on myBean when VM terminates
    + * 
    + *      performance=DEBUG
    + *          --> Output all performance data - after each invocation and upon VM closure          
    + * 
    + *

    + * + * @author Derek Hulley + */ +public abstract class AbstractPerformanceMonitor +{ + /** logger for method level performance summaries */ + private static final Log methodSummaryLogger = LogFactory.getLog("performance.summary.method"); + /** logger for VM level performance summaries */ + private static final Log vmSummaryLogger = LogFactory.getLog("performance.summary.vm"); + + private final String entityName; + /** VM level summary */ + private SortedMap stats; + + /** + * Convenience method to check if there is some sort of performance logging enabled + * + * @return Returns true if there is some sort of performance logging enabled, false otherwise + */ + public static boolean isDebugEnabled() + { + return (vmSummaryLogger.isDebugEnabled() || methodSummaryLogger.isDebugEnabled()); + } + + /** + * @param entityName the name of the entity for which the performance is being recorded + */ + public AbstractPerformanceMonitor(String entityName) + { + this.entityName = entityName; + stats = new TreeMap(); + + // enable a VM shutdown hook if required + if (vmSummaryLogger.isDebugEnabled()) + { + Thread hook = new ShutdownThread(); + Runtime.getRuntime().addShutdownHook(hook); + } + } + + /** + * Dumps the results of the method execution to: + *

      + *
    • DEBUG output if the method level debug logging is active
    • + *
    • Performance store if required
    • + *
    + * + * @param methodName the name of the method against which to store the results + * @param delayMs + */ + protected void recordStats(String methodName, double delayMs) + { + Log methodLogger = LogFactory.getLog("performance." + entityName + "." + methodName); + if (!methodLogger.isDebugEnabled()) + { + // no recording for this method + return; + } + + DecimalFormat format = new DecimalFormat (); + format.setMinimumFractionDigits (3); + format.setMaximumFractionDigits (3); + + // must we log on a per-method call? + if (methodSummaryLogger.isDebugEnabled()) + { + methodLogger.debug("Executed " + entityName + "#" + methodName + " in " + format.format(delayMs) + "ms"); + } + if (vmSummaryLogger.isDebugEnabled()) + { + synchronized(this) // only synchronize if absolutely necessary + { + // get stats + MethodStats methodStats = stats.get(methodName); + if (methodStats == null) + { + methodStats = new MethodStats(); + stats.put(methodName, methodStats); + } + methodStats.record(delayMs); + } + } + } + + /** + * Stores the execution count and total execution time for any method + */ + private class MethodStats + { + private int count; + private double totalTimeMs; + + /** + * Records the time for a method to execute and bumps up the execution count + * + * @param delayMs the time the method took to execute in milliseconds + */ + public void record(double delayMs) + { + count++; + totalTimeMs += delayMs; + } + + public String toString() + { + DecimalFormat format = new DecimalFormat (); + format.setMinimumFractionDigits (3); + format.setMaximumFractionDigits (3); + double averageMs = totalTimeMs / (long) count; + return ("Executed " + count + " times, averaging " + format.format(averageMs) + "ms per call"); + } + } + + /** + * Dumps the output of all recorded method statistics + */ + private class ShutdownThread extends Thread + { + public void run() + { + String beanName = AbstractPerformanceMonitor.this.entityName; + + // prevent multiple ShutdownThread instances interleaving their output + synchronized(ShutdownThread.class) + { + vmSummaryLogger.debug("\n==================== " + beanName.toUpperCase() + " ==================="); + Set methodNames = stats.keySet(); + for (String methodName : methodNames) + { + vmSummaryLogger.debug("\nMethod performance summary: \n" + + " Bean: " + AbstractPerformanceMonitor.this.entityName + "\n" + + " Method: " + methodName + "\n" + + " Statistics: " + stats.get(methodName)); + } + } + } + } +} diff --git a/source/java/org/alfresco/util/perf/PerformanceMonitor.java b/source/java/org/alfresco/util/perf/PerformanceMonitor.java new file mode 100644 index 0000000000..2d616edf67 --- /dev/null +++ b/source/java/org/alfresco/util/perf/PerformanceMonitor.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.perf; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.vladium.utils.timing.ITimer; +import com.vladium.utils.timing.TimerFactory; + +/** + * Enables begin ... end style performance monitoring with summarisation + * using the performance logging category. It is designed to only incur + * a minor cost when performance logging is turned on using the DEBUG logging + * mechanism. See base class for details on enabling the performance + * logging categories. + *

    + * This class is thread safe. + *

    + * Usage: + *

    + * private PerformanceMonitor somethingTimer = new PerformanceMonitor("mytest", "doSomething");
    + * ...
    + * ...
    + * private void doSomething()
    + * {
    + *    somethingTimer.start();
    + *    ...
    + *    ...
    + *    somethingTimer.stop();
    + * }
    + * 
    + * + * @author Derek Hulley + */ +public class PerformanceMonitor extends AbstractPerformanceMonitor +{ + private String methodName; + private ThreadLocal threadLocalTimer; + private boolean log; + + /** + * @param entityName name of the entity, e.g. a test name or a bean name against which to + * log the performance + * @param methodName the method for which the performance will be logged + */ + public PerformanceMonitor(String entityName, String methodName) + { + super(entityName); + this.methodName = methodName; + this.threadLocalTimer = new ThreadLocal(); + + // check if logging can be eliminated + Log methodLogger = LogFactory.getLog("performance." + entityName + "." + methodName); + this.log = AbstractPerformanceMonitor.isDebugEnabled() && methodLogger.isDebugEnabled(); + } + + /** + * Threadsafe method to start the timer. + *

    + * The timer is only started if the logging levels are enabled. + * + * @see #stop() + */ + public void start() + { + if (!log) + { + // don't bother timing + return; + } + // overwrite the thread's timer + ITimer timer = TimerFactory.newTimer(); + threadLocalTimer.set(timer); + // start the timer + timer.start(); + } + + /** + * Threadsafe method to stop the timer. + * + * @see #start() + */ + public void stop() + { + if (!log) + { + // don't bother timing + return; + } + // get the thread's timer + ITimer timer = threadLocalTimer.get(); + if (timer == null) + { + // begin not called on the thread + return; + } + // time it + timer.stop(); + recordStats(methodName, timer.getDuration()); + + // drop the thread's timer + threadLocalTimer.set(null); + } +} diff --git a/source/java/org/alfresco/util/perf/PerformanceMonitorAdvice.java b/source/java/org/alfresco/util/perf/PerformanceMonitorAdvice.java new file mode 100644 index 0000000000..8e72847735 --- /dev/null +++ b/source/java/org/alfresco/util/perf/PerformanceMonitorAdvice.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.perf; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import com.vladium.utils.timing.ITimer; +import com.vladium.utils.timing.TimerFactory; + +/** + * An instance of this class keeps track of timings of method calls on a bean + * + * @author Derek Hulley + */ +public class PerformanceMonitorAdvice extends AbstractPerformanceMonitor implements MethodInterceptor +{ + public PerformanceMonitorAdvice(String beanName) + { + super(beanName); + } + + public Object invoke(MethodInvocation invocation) throws Throwable + { + // bypass all recording if performance logging is not required + if (AbstractPerformanceMonitor.isDebugEnabled()) + { + return invokeWithLogging(invocation); + } + else + { + // no logging required + return invocation.proceed(); + } + } + + private Object invokeWithLogging(MethodInvocation invocation) throws Throwable + { + // get the time prior to call + ITimer timer = TimerFactory.newTimer (); + + timer.start (); + + //long start = System.currentTimeMillis(); + // execute - do not record exceptions + Object ret = invocation.proceed(); + // get time after call + //long end = System.currentTimeMillis(); + // record the stats + timer.stop (); + + recordStats(invocation.getMethod().getName(), timer.getDuration ()); + // done + return ret; + } +} diff --git a/source/java/org/alfresco/util/perf/PerformanceMonitorTest.java b/source/java/org/alfresco/util/perf/PerformanceMonitorTest.java new file mode 100644 index 0000000000..705558ef42 --- /dev/null +++ b/source/java/org/alfresco/util/perf/PerformanceMonitorTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.util.perf; + +import java.lang.reflect.Method; + +import junit.framework.TestCase; + +/** + * Enabled vm performance monitoring for performance.summary.vm and + * performance.PerformanceMonitorTest to check. + * + * @see org.alfresco.util.perf.PerformanceMonitor + * + * @author Derek Hulley + */ +public class PerformanceMonitorTest extends TestCase +{ + private PerformanceMonitor testTimingMonitor; + + @Override + public void setUp() throws Exception + { + Method testTimingMethod = PerformanceMonitorTest.class.getMethod("testTiming"); + testTimingMonitor = new PerformanceMonitor("PerformanceMonitorTest", "testTiming"); + } + + public void testSetUp() throws Exception + { + assertNotNull(testTimingMonitor); + } + + public synchronized void testTiming() throws Exception + { + testTimingMonitor.start(); + + wait(50); + + testTimingMonitor.stop(); + } +} diff --git a/source/java/queryRegister.dtd b/source/java/queryRegister.dtd new file mode 100644 index 0000000000..9904351979 --- /dev/null +++ b/source/java/queryRegister.dtd @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/test-resources/Plan270904b.xls b/source/test-resources/Plan270904b.xls new file mode 100644 index 0000000000000000000000000000000000000000..a3bc6023f6e0c612211ff0e3a883314939b09f95 GIT binary patch literal 2460160 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 zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>i7V6{})Ztcq2!Dptj+SQV>cRji6tu_{)@s#q1PVpXh)Rk12o#j02pt728GidC^H zR>l7dtN7r-|9Aha$xW@z?3Xz-b3o<_nFBKiWv-a{&2NAE+e7;QJ*@xV{FZ)yy|DwTyi)Q_I^=GW7_4o!R%F`u*?aPHvF@t=l|% z=7Q;6!wqxu)4%)oj>w-oYi8U0+``~*^n3f>#TQT})7eLQE!j+t%K-P<`F z-$h%HPMz7Y + + + + + + + + classpath:ehcache.xml + + + + + + + + cache1 + + + + + + + + + + backingCache + + + + + + + + + + + + + + transactionalCache + + + 20000 + + + + \ No newline at end of file diff --git a/source/test-resources/farmers_markets_list_2003.doc b/source/test-resources/farmers_markets_list_2003.doc new file mode 100644 index 0000000000000000000000000000000000000000..f6ce700a1aaa8ca4a158bfac68f9a946f0114056 GIT binary patch literal 30208 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 H<-PxZXyxrC literal 0 HcmV?d00001 diff --git a/source/test-resources/filefolder/filefolder-test-import.xml b/source/test-resources/filefolder/filefolder-test-import.xml new file mode 100644 index 0000000000..3a7959300f --- /dev/null +++ b/source/test-resources/filefolder/filefolder-test-import.xml @@ -0,0 +1,58 @@ + + + + + L0: File A + contentUrl=classpath:quick/quick.txt|mimetype=text/plain|size=|encoding= + + + L0: File B + contentUrl=classpath:quick/quick.pdf|mimetype=application/pdf|size=|encoding= + + + + L0: Folder A + + + + L1: File A + contentUrl=classpath:quick/quick.txt|mimetype=text/plain|size=|encoding= + + + L1: File B + contentUrl=classpath:quick/quick.pdf|mimetype=application/pdf|size=|encoding= + + + L1: File C (%_) + contentUrl=classpath:quick/quick.html|mimetype=text/html|size=|encoding= + + + + L1: Folder A + + + L1: Folder B + + + + + L0: Folder B + + + L0: Folder C + + + + DUPLICATE + contentUrl=classpath:quick/quick.txt|mimetype=text/plain|size=|encoding= + + + + DUPLICATE + + + + + diff --git a/source/test-resources/quick/quick.bmp b/source/test-resources/quick/quick.bmp new file mode 100644 index 0000000000000000000000000000000000000000..9883f34ea73a6d52ac4f48228248486ae04278af GIT binary patch literal 113030 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 BeX#%l literal 0 HcmV?d00001 diff --git a/source/test-resources/quick/quick.doc b/source/test-resources/quick/quick.doc new file mode 100644 index 0000000000000000000000000000000000000000..eb307fb2182f673a4e48d950b629612925aed126 GIT binary patch literal 19968 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 literal 0 HcmV?d00001 diff --git a/source/test-resources/quick/quick.html b/source/test-resources/quick/quick.html new file mode 100644 index 0000000000..76c633d74c --- /dev/null +++ b/source/test-resources/quick/quick.html @@ -0,0 +1,17 @@ + + + + + The quick brown fox jumps over the lazy dog + + + + + + + +The quick brown fox jumps over the lazy dog + + + + diff --git a/source/test-resources/quick/quick.jpg b/source/test-resources/quick/quick.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08473b8e8b65fa2ce2cb481ab31e5b18862e4515 GIT binary patch literal 26445 zcmbrlWmFtZ)Ga({(BLk?9fG?Ahe3n83=T866A13^?hZ4!L-4^N$P5lia7!Ra9v~!N z-XGt+f8Vw4>9xC7SJl~lx_WhW_vzh#H~xMB5CT=yRRAa`D1iT20DrduivP3q|78C| z^8ZBeKhA$g07O^-3zPyh6ea*F5ega+%HIh9{l6elQBl$Uh5G;7D=bWGbPN0fxK5qLu&!TA*|`)Y;bQatBuiM23Ydkou#ms2aA8(>MgOh<>jr`8_Wl07D8?#K z-wg5G-(2!+(zzq@(s-objL|TIVzzFuTfMevZ)b%wcO6#a8++YJKre5Xh$~LKnYd}9 zrI;h^AKB;9>o@nofD=I5xdYh>aWFVXJj`O@-n(H`7CH_xG1n|<(X-fFEV7Ip>lAVF zPR1t?h_2}7Bfn0dh4gza@GkYkGw-df8R|E8b2ZI+qSAh9U0<5MGBcZ=w@DI?`=8;zqgCesr)je$Bp0#ef z>uA9K`)(!!Mv?KBD8X4Tw>J1dOfry)E`V3}sziYe*`Q44#KN(LDGDY5zwx~%VXfY& z61QpyOBC!gCSkMKU_vM4A&F{Nr+TCt!%}!1@LRt4`Bg&tx;@Wp=zxGNp+Wd`CNlj# zvoR*M{iu&hQ?r^4HAt;FCfdSgEX3%DBisB3|4Rg@={Py=waT}5HpFaGhE3k# z7hIVIKqkM~-dzRGhsL#BT$ZNn?0%yO@T+9?CEjvbMg$*RvEfHsCOTO5E5{O1jWXub zMYJz0O!oI|tctnAVK>M^_$0GiEo7iJ{PN9xF93Iv!5?{EfA#h+06VwP!f))U83-N1 zprXHPQim`KoY>U2)Du-sAj3{(D4fCXW^=0Z!de9P3%&Co++%_PeyfWrr1Wb zYTyRwVik#uUdC(gLjnl@0!S>K^khoC1o>+fX}KBm%M$DrbMd*hQ`ZOh$rsj1cXLcq zmF?aN13!)7P&>X70;z?B`s1eytAGBiJnfY*KpGtgt70+65E_D12Is?BZb>dmpBo~L9fdLIVVJr`7Wd_LPQL{fEGeObFe9>+pRMxmq z`N`oFy1V{l+E4xmkyG}m+~96rMT$q}%Bl!HS`0zGU+F406Eewsuku2y0=kf0&Y^u?3I zat$S^=@hv$$E?h?Wn;5%1HHf^#kT92$xUmuX0uswmTA?Sj`oB_y#3#2;z!J+?Wp(utbP7{(TDU;Z)$WTR?d*)SWyp} zybh>xX+q5Q#RRfKBX0HE#P9KP+lTCcGwp7B)b_1Cq$?H*B^oY%TDC582@qF3!fzQT zYuUVz-e^vfF@x?Mpid5?$)sD+7IkC5_!IGf-k@bNa^2L)ZJVpjEt4zw{q}BxsLz(cnG$eM(6H^K@gr2q%|l5QnbWKMQP1Ds zWtzg$UtZ6R=A!*e0D&diTq5y`=5hoRca>VLxY`X}M9X|u4m}NCyKeQAz*Wbp<)vG@ zWf78^Ub0|s%N4G`^5{4MIz70?!;Je#T^PCneP?1m z*jf%$FQC^5mrY>GxsA?3uezafh~Z4v<99+IMJT-RW!&VU zAc>`l9=$%k<#bV1Co{9LAn38IFfFRS~E|FZf?nX^l zq6HY+m>r_3RDQ!%#&c3}G%Asz#%J5TEQZW9b zd%s$*l7H@dWkoAy2%d!#SnH6}5W>6He&vhY7&w@q;d&EokM605^R@R2v>20W&{+TB zmz?XabS0yDC3{DnVJfT^HrodJgjGD4)u@k>%Y?aaOT9_`L9`}>Z{fl5oqh(vbR!;Q z;h}AY_0l}9`zWe54AZ2)iZ5>VP{;MV$6521(d8o=p+`bKYi-vr|1>_5^ZFUD!Rwr& zlX}F4v|MncROvuY)8$cL#_rA4;0ofZ{kvYAxsSNJYMJf)SDdBGkkvBC$1gtcP#j|! z^D_L)uRJrh2+pp-?9K~!2hHwdGxLTqvf_qnsWcaNyIJCshqdOnRB$=|S0ASOxz)XOH&Gk-{hFC-xxE)ip=EVtrqrt< zDu#@a=t3#CYen}=JsSgOyg2kR%~S(n)nY=(w?lp(80CWxdL^wK+pJ03*61@*Zcd;;MMXRGfGZimoi@u;XeZYU}5)T( zF~6DJ^_pbGucQ3kpkYK@_UH0I`uEKRefgzj8L}^+k&Xq~0s9Vl^%N#d+yHBl1dRkD zegO@^br6|&cGC?ah-KJT%Jvs?jx}Q6&~l*Eb6A3ctL6L<-!bX1qGVAm? z0Ct1*9>rkb*Sbr(g3pHDfdsG~G9@-xm+n<(qsM;MK9MNC+rzNq66@X?B?e;%l#nqv*O2QE-zs6xcdB_~(31O-~ezRwmq1Gb;H)*Ntp zVCxxPEb^BDGh1Xyiw`3Si-V@GW>XJi&0J3MA;-vD&Eb=cAb4gowA<>sx$}HAc6ZD$ zo6|%)s0OHW))oZqt5|ZB9SW288cMoYvTJ-0ah$FgqSz?{tqyj%r#70f$rt@1di`nW zbxlo^fPMF_pRilZ5p%UT-;kbduOh^ky_Ldb=n%#6;*dARvr{+3jHG2?!^3$RmCV24 z#|iC6gY$MXBcGfK-=Ai-c>3IYGGHcS>(z-1hCHZ3*UrK;9yhv2=Z{WOwmCK?M}H7| zI(r^vBwIr~2jd=V8-(novKhlpj4~JSGZh2WAL1w)Z(eKUZ!edjx7EEwt|qiDnHFfA zCt(>w>~&KF+zkW6 z3xPskgET)@i|x7vD{r(!qJW#Hk>*JbXZi>30;uC0tn0k?1Ie<%=oX*Gu~aM_U~@n zND<5#^y|SyGsb^_=L9(7R(X($9lKniUW?jHS8pU%KCNYM1YD30Rj}(>`qTegu7pn-G~V7X4KR-`WC+wqNqlMRRGo>g z@5ILV3qUY9l5_VuGE^$PG^eMJX5Sl%0jSVX$oba{oy7HN@BAZv^k@40#M}_r&h`>n z{oS3)788;8dXhUTaxq5R|yHn5RC zue!TI{PcZq{;GlpPMNLmV%08!!*O)Ihi@1PcAchiWBRhq@TvnR(K4QGDuK({$04?Z zw;NZKr_Gt@u*SqDI#+Nf&Xq#mw`=abTS|WL*L7oIx}T5I(ieO~QJaF6;c<=~s|{C% zmhC7^@_+_1+ir4(caii;wj)c!T{UG+ax*(5fIG>)(QfBc~|2`k*hijS<8iSFb7x{s6<(O-)`LF zoOP2J8+YB>aW&baq?Ap)4DX|TgNm}o~(QKW@KH&f%M{?u-&T4KDB? z&Kx=j=fj>0{QdXf63B%3R%pw3X-x%CTW~-5>E>}(+6j>c4|;@)ASv&(96WZ+Q?1Q|LuwS~qNU+TGtgXwk(y4pLe0f$M=?)ZX?fMv6W^l_`;JlPi|k$p)uZbqlh(fL8pf!z z)py7bG>fYvzw374jraF~1_}w%CfRU^peQYgTZrpRr@`>zn>05c)*^z!`3AT{Z2bqb zALuu%IXB8{U#ljoMNtyJClP9uvR}}*4{rxf%OM@aUgg?p`@gR0r`?9e5ji(Y)CTx< zC~gw@BohW8AY|`zvgFL6;v8q)P6#m5RYfiGSS!%Q1uES+kBjn~HF=Rcd)&QyY*)cL zh}&HA$mCavqNz5WIV9*8d2D-UNF&fYcgXpdV40D` z&ozq1`1H!?uN9$aJ#aZDu3OiAhRk%(?^0iDZ@_2P#viQkm@QWP+qNc;ms>>PkV@W= zLVPf!QF~F@FaP^I=$ml)C{fFXZCV@{ua&a3OX&ByLm|xJSP-o+mW%pbwekdp=(=^A zKj~r1YIuCFjd`T(A!3SnH>t&$Ox^AVkFy;={%DO8Wl#{eu#h%O-ca&-_ib8&F(J_0-0F`a^^*imlf5&n;Yc?0D z!HHk;a6-Ct%2o+@-oB?GNnrZk>|sz@k@3aQ++NX0j2D`#*(|RWp%T`A zY++?&&P;~QnGkVi+^Ep?D-+qC-LjcZgO?;=HNRe()nJ#9yQm{8TG_o!tW1-Aot||& z_<7TU{_S(#59dj-VXeJq;UoTwIVGoDm*?P&SK5b0xk)@4)T3HKG0_s^6AVqq@!ug9 z@CtMvuM)MUV&34%MYuVc>3!isOkVeUON;F;3t$!>pSAzqNVq9!JMd0hl5;w%F0~*M zyX?Fq4Iw&YYO|yH##foGMs%7FA$+D@k!!?ek+qZfu8+w_?Iw84aW(2RUNqgv+W^A{%CRc$8AS@ooE<_Z zr5exOra8J-{95bkmG(!&Hkzl-d35ZcRdcq-z>T18{4Cn5|F*l@x=(;GC6@#3zp#zPxn+P`b*oXBHS=(6F6w0rplb;Ckkq;V z)Cxn)4_=J`Q}|2@7K5W@fke4`;d^2xS7BWp8HVAOxswdoL;B_4{Um)C0^bSf3kEc} z4Up>e%n|vE>XdcXqgB))iZr|$($*byM2%=(XL*7lsuvEwYiGBR+=$p6%KX$3ME9qT z&EzT*39bV&8CQ}DC}g22!KNCp>@aSaQ=4B(t1g=RpJHo?#%aDl*`RFrTAa~@u^r^9 zb@iUkucChI&q}f`38XsbBBpfP71>j9yR1G^8Fw%}xQSLmWrL~RV%xkp9cgPddj!le zH0p9oaQ~MYpm5j3zG<>s)iUQWW+~~4;x*}LW3gfd258u4^NZ$nwMG3YyZL=?y7mk8 zD48vWZGhT+$u2KrZd1kDC?|xu&O)Oqd2+Ktp)z9ao=(cohx8|xLrsa4)pAG(llD&= zC4mRj{_EqY2yd8f@>|wMUPV%@!Bdvn&Nl}QE6ci)lwZ)INIE5L!YOE& z3fO%-aQUD+!Okj-I&W3`J_ke$kXa3AlsFDf^R}q0XeFn!-W>WwcaVDd@M^ryxbUqW zR8GT+ny{o7bf*HJ3DcyUmvw1;Pboa!EMJZ>IH%!lD?Dq`6>R4;y!`KXw%?;!W`WaOQypX^fRC zV3FTjJIAANuB@wJvUgaUa`6|SY+SsOD{yND=kxv4I(F^~tH_FI$(51FoH~;lcl0Ei zy;i=UoO54VpEO)pRHkT_BGDyMf4Ig9U0t4JC$SN}h{%W>*VB@4jgap;t`aCICT{jI z9w!53OY#uc_$vkwM!A5#a6ZPS3~&HxTn#uWbpT}^zQ5Heb3})OEW>GNf~f>HtDY`J z^!p>bxlY1r2{p3b-+)&*V=&B{Dqm?9&_A2{b%?k)eig|h8MUkKf8dabF^ID7PHNf) zIoF;mV)W7rLv=M^iWk@SwzKMSa7|X0@*qPS)*3G4RU{|XHR<+^r=|q}A9VskRZv1Or&rljXt!PPVl@0@T7`zQOZ^W#ksnB^+mr{CI7r&Ip zV7=RXf+BvHJH;)^oe2-!YfA|41ZO5P^KO9{GdB(=Y3aNgkNyI*<82Bpp)kXkb=~?> zqR>?T;_>0Fu5PxHcP+;6EtZfol^v8DRy2gow)@b`zMPhyAG-^88%U>wc}#6J3jFUhy>{y{(N-gOU1L~^iLGI6tR%iN z(r66R9jYl}9C>=^K%#nyI)Z`CqATHKGZ6c#`gc&rr22zH&4Vm@G=EDFG6W@5uL+^- zQoiP9SqqZ_T6u`gUf{lS!;P-plbw{T+aM+?nvUpYnt04^bj)Z$WE) zUe)YhpY}|7RS<^;hYg-vX@%fMmpHc3^*T;9fm}=)hF{jEo1pVF>UbL-hb;EQ-WP0W z>2jXF9j_CSvAN6W+MN$thTKOr_V=!==eRmes?&DmeuePB|b<$v~JVv?u7Y*IpAeQCzk*Q$~xI#Ri z^=Hac^@-?_ew|K{vEdxIK>kOnlPQ^fH(tyVHgnr8uI?hO46*Wx+0x1-EgMgvZC!qY z5vfj^?2+Df)jj`Iko@yc;0dT-DO!zng^Tw1WWMA|VGY~QwzHLsE4Oh9>&Hjn*(=4E z6PIqN=C)ykAd6NH=bd^|obmy6aJ+1qJ^yA1B{=39fjs;AWBEJuVV=_gY~Sb9HlA7+ zTbDAKR^trJZydYc_%R;xU$!krF-Qjf*W{R2mtk^X9 zJv;wuK#W*Bi470>{kc78LNRcV~`f|R)`#C57!nJA~%i3gQA zXpl=z-dCM|d&^|3D!Mg;m!-Y>85&I!`M-g^KY~Rv(TE()>0Ih>X-nvE&=_bPhUv~- z!=ULc@ER}FWQziR1rsjGo%Z)k7vH|R?kV70#R}Rwyh7LKqOz20+GSI`;-A2n$IsuJuzlb{K-jox-1Z9-_)|#D*JjM zuB*92uFk(%rEXa$PylxMif4rkn@4eyX$3H7-i+m*&V+-9&E`X{n_HZpuvpjYO5s)m z&#*9MA~~I4!;sagxbVc&KcFOFfd-91Bu~aAP({L}J5#u6-HsSEW*EeIM0w6KoO>7Z z=vy)rt*>T-Ft~|Q?mYU?N~9ZkrK!>yX9=Ii zGhz)>%YV4tdonp-at?S|C%mXflLvBD{~UME4V9Hnu*^}d(MUb!54$psBz#UaHdhM5 zq=%U@Aa_J$KY@7OnufL`bln)Q+Dk=fd2`!Zcydg=ZC}SC3Kohp7VX`18dm;Q#x_4pnf4u zx%je6Dy`&(k>v7832x|N%_=cC6;7Nj4DT<=zC|#v2w=HWKaPJR@z~@wMQR?X<`EXW z18AT}bKys5wofA?%qPR-Flqrlm7>}UF-)9f3H>?FKLqOfm}vT9 z?fJ$UfVm-g{DtZG5>rVb62<#y1$>{(dP2fEKK5YmXKWFDf@|WdyOneBrDL0t{jjuRO%bYpCl;UjC^*tV zl@WiFscuYZeG{&46`FJYK&yDbq-x`eqbFo2fuZFcFBQj9tFJCF_h6gCu0UCD(PrmF zK^B7&G>bJ4D0!=13PAtxgT=GO05 zV_`b9{aI1*^Sovo38ZL^A=*49oaCo_AK<0k5|eaJCWPwg5R=G{)@VS)ZYu97pf&E)JG=*8fHsfNz9~fWYU@G6?D8MEaF*NeoUAPZrM)LPgtx>;qzRC;nQ0zM?>0Qp)nYz{%oG z;}TO@6xT)7nOAn}BSo!mvc0+p{S+_eqL|3h-^^M@I*i$_$??I3*)%tloK(M{Fo}or z_PEoIK=T~8$K8gVUGG=nU?~v;I^`z9^HmX!Ry#Q@S~m0kmz-lozXm6)c`FYY9!iP44F+g3ys=kyp}aGaZ&9)Tqbpw&($n-SCAF50j7lEs5^5w& z7Z{{c(u$a0qL-XAg`!}%laT#}Gi>BfV2G1uO;j(Xf?tGVELI;9Q&?&W&!n}pyH915 z*d|me=SxBxJTSRen*D%h%w#hl{ETR>9_t9xlF&4fx5{-$lw4kHf}(cy`f%N>=U!)~Q_T8mgVqI-yv4z47YfGy7?*^*^ot&&D>iUPuirkiB z0WYCU|65TNR^%^UYkHBz3nOnho)j-iT;CE=M4LGLvayUTsa z!4R-yipsV{<0@f$Yaf>NE43+`5k(rfga@2uHt_1x>YyszZ-`DV1ranWX?A+(`{GtT zX!)hxXlx&xemxQ-6B@$nF2P^yqz3 z+)^5NkShHT;y3?rF!c{<^y2@kDhZQ;APdj#*v+_uEsrjcFkL{$_Xw8HEk^x#J!#lT zWHY931$B__B(?&H#35xon-T)gG+wa|n7}$Ma9t!d?|7os4CaEyX87V_PU&^U7otuB zirfNSTB8dLKuqyd8Y#asZVz~;S3yv#wfe;Yx;tndEyR3CB%7A4eB2?q6uTm`0W^Um z{P*fgE4#r$If>GhQ`3YZTCbMKWH$7e9I2#`#pIs@se#LIK>L(T@tpzK8vRH{qr0C>a%jm=b zdE2Sb*1_}m)XI%j#CSSEMrEeI(rnzDjT~3d0pV5J2J(suKXeVfg=7#45T7Rn^AZh`)lO6lbM?_@bwf%m5Ar| zA87SEq+~Za5R2h+H87eQ$DR}At40B#BS5_T=o#J=?Ca!vUVvj2I%p&^Rl>m~P<7OTdc8@Q*@QGxb@4V4j*O}C**aug+#NbO|~ z$z{9nhZ%@;lqa9DYjBj_WqAB5gJYHYmagXcjIMtJ4!t{Oev9jFEde;Wae$Z=mV0^A zVuA`K{8VYCPxb>!%Ro)SGR>tYcu*pJ{*oJ--Ib(xzN!qly2J#t#IIqh7sxDp}*=q>M!A>$vM7-upGcwUm;i`DvND2DJyw^CPT9K7IgCps+ zR1_L+F0Q0Xf3${QdWIn7D;@$a@~KOieP_2%Giw}V<#r;lKInHS{zsXY_pPhHFvzcQ zn@+cEA&7^{?uj$o_*rrKPb6rU%x|e8nCMp`)vt#I(IqJEu6ISXNBxeAKm|NLoj24c zMxV{qvBlnpbcquw8{uHNP zj+4w9c5*%R@dh8~O9pL?Q}q%f+wAKMrHt4?@8-43OkIIq&^%dnihLcY!A|y^d%f#P z>fq;o1g&uBz(XWM-`OMRGG0_i5v@rw+;kr`b78NK;hiQcBcAul#{Prf$bd zPEN9h^{D~CoM=&=GB7u$n+u7TDuHaweOFqDT+_L)_4?9E+5oZT(KA*w*%YG?PGUFdMB<&kpVcq#n=@!E7HujoqKs7*BDMgRG3$j8fw zUGAC5ScPcT=ol|A1L%@CesZ%l&rmn$&uWrN`cTZt?+jrq&jjZ0+G^ zy(1_i1uVM2rQFbByYLZ{*w-0N)V~UBO7@EZR)or0ikLaqF>^rj2~Tyx`WJw|FK~N| zXApQ5Ha1t&Rr6Hl#K_* z5DF_rQbg(xVa?Y_*U(Q3+E;aKnbX~6fgs`E`9;^*vE_o!u-?$;x67tzM49NvP=f7+ zg0_bk$&8Q4wb}Z)%kSUTM;9A*nne@5wC>*ok068`37;l`ZEsH3GcXDTPxiXoC&TgC2Es5- zCvb}4b`l^7j9&DLnDNQK@!ao?FIMBKi*PtiVUEcF1du+3#W8AQxG)C^6CLY#UHr{} zv+NvWmL_eRc%nu}*fyG&{SBXOfyHsV*~uvqSaQGq6e@0BfRneL^6wcYn5_6&yVV-1 zp1iPnK6T*jRnt6e0xkHYHhKaa#WRs?E=b8y%XT1R{EpE1p7YjkT;9j*BS}=B%Llf= z+3M!j7zJD#EVsV^(03{4*FV04D2&_vQhYj|Z0q``P%$c>bzT|uKKOTeVXCMssu3@b zY&Nizm59egGe%&B4cU{l#9@uBdXA>QDcO{I=?@>rXT7p)j#G#k?2glW5J{e#T~8bm z3P2`flR>+MZfzsH-6c1@1HagcB~@>LP5iV5H(Zyo$k`m!Byf(lyV4AbH<6Mti)l(l z?IDVmVSd#vg}+zy{L8gu?!>BQrC3jYD$RsgAKP$}tvjm4>*AJCYe6*osN6+zsm^~{ zff<|(wa?8sgU@6_Y!S)|M1(tSF@8s#mu@n!dNUeWLnR1GPT5S|&J@O?CaHr{Xr$D$ z>*wxkcQ1ANN6mNR_)c4qmr_%ua25GQjl2+XVNzt5=I8PnF+$Aq$!o$!@Vgd*Sk+hCk^|S7kIvID(o?QGn@PHQ zrkpFE{=2;w7oFS+(WtEV7(Dv4s;g`k+ByGK(VhRB@y7wL5yhs+22%aoDS)t`W>3nGjUs*9 z@0h+t1hC;J!z6>&T#3bSdM2%Uq}Aj1L~zm_=qbq}!#~IS%CRl6p5nrS13*LN07{2PuZLvw3;{d^G} zi);f5$Xw!9|AiWJ`*d|oi~2oL*ryUGtVsSe?G0(WKPZRu0#8L1!KfpFO*A1R0OsU* zd$gYV$**6>%JmN5=bAG@=*XQDA`nFVdjYFW<2WaF0Rto6n8#IlvBf{?eWXt5F>MEPm!G11Ve85*WquJ8`X#?P)7RO9A1;3OZuI=oB$2)6eJJjguAEMw?@LM{N$-J#G+}c6E3=j$ zbTtW05nx0lNdfw=%Tzr+t09=>uU$}+)QN+huqEHuX5sozOMv_j@Lkv%)<9=}@6101 z%!@becFcmV2XT|=Bw9?N$!gaq(FHkkZ;o|Gzb4D%X34;&mK$RB=*|LStiQt+Fl<$S zTQQmEG`R9*wR_*OUUC_KqD)*_b0X`9-qp3I>4_p6;kWPk?`q@uM>gFToSq^O1Il^M zJE^A-ZI$f-ZaK%RIquGpX{oj*+%$6LFW;4!Tjy8D>*Qn5gUH#XkAe6xcwVe?QDpH~ zvqoChExdPc&q8bKamL^M2bc1Tt}lPF|fJQPi&y8DwL6H z_%I9q^w0IHTd6+u!IhE(n2b*iw5p;+fnq&YXc(%!%@G4rFCHtRarvx&Upt*SXnx?Atl z*KO%~BiOwVS`X&KtY+-8zZ_flV(K(qweie~;~BpDEz6h6(S_^LlCjw7 zx?M?&B>e@fzm#?LV7VP?^}yaOGdPkt=g{s7xfoq;_P_T)D3dskt5{ZyK|c?S`wq#P@quD__Tiy6p8WCqrEkOOE*!`$WT2vWhPLRkmCw zfs@%_LdQzx9Se(E?jUX5{0tgL9v+qCIW1MCLt0`AlcXQ(1+t3Ez2B~4XoTl6&qhvQ*UOU%A55lCRu0=C6BX{dob<~6m%UliJ z-o4(R(K8eWcm_Wv2Jk@dx-^imGs`Yy<*9p*FR5k1>p5t!!}|>*)ZC(!C=hYOc zN}g_SZ9IPA*Gd{_*6butNwy1R?vtYa4i0!-`b-wdpMUxO`eNe0o9$RsQ|q^H`rVbh zZzBZk^%5zOdx6u&1s(Pm;rDu<?$v<{c`?`Kr{I2ag2iJwVqUx8h#= z4m3zBjZo&ROVMU^5Y1x`;`iMn8h+(U2nCcIq+&NeC@ZT>Lk6MDeRk2q00v zTzQyR^F@MGs&BY!u6OMAM)+I?N;?Kyg50f*P)CvUf>PN=K8 zRLLj5{mk!tP8Z|IbY1b6?KGT*x}o?gtZH$8tDR^qtrl@&#D}5qE2*<8Qll&oZFfr4 zoXqZTy7Bc~P>VrVnsG0>X+nW5YAvH2x`7gMNSO%|BNsvPEcEi3JnLxrq!fjz;H+!b z9?K+_X0HVj1GUB+wL2Y~0A6-mk0Cm0I=zKH*ihNkj`_Hwi$x z!HpC=L7!ves*sj^l|e_<7a1+bMvmL_8i$A|Mu4;D&)X&{9o_!X2#+L{6r@X%J07yME6UlaH3L00VfotTNJP=_edj`h=QKZ(wGphK?pLbS$$1~kR@Jq`Ib^EJ3oSxzM& zZ?k#R^_mN{+yoj~H-!Elje5PlrnVX<9;QFfy8$M+*THX-KRze7Y4?jC*Khs;dL;Ji z9|>wrG0vi%d{ms(K%0`O&5Q4EbvRb_nW;@_bKs#fy)QdS&+Q|jLhe>`*b``NSVoz@Cgp(7VQhK;j!O< zQ$~(piZKasMi9k6W^%^I|8bh%PYwR6z9{|F_B&|lep33sPX+$pgkkbODc+xt#~)Y!B@~a|`lELbz$Yfwpn*+7h)onSd&fMG zB@H?2Rd07Os#Zw`5L$n-!YrwCc2@M$Fu9_3r6H9~)>)bA&*H_4Bfr%^)5DuXwdIGX z2d>+-CKl7Egw(4K5=Sx1F}pU#O{k*DP(ttj?vgkzuMG)}K4Q}QZUcrZ1Pu(wX_9LpcRdg3M#qnlA!sIlTl^GOpI3-R!&6y%?hIJ=6-YWIpE zxy@LS0@(PFLgZ$W;G#aA>)|GoAB@AkK~+FKSom=$U3D--L`Q*SPT`(@lT!da@#) zJIU?Ylw-H13x7_n%=7@{u5PLhF8#4CZR+ITMKR2uxYt#+#uE_Y=iV!=oMlf!5Bh1v z$MRuLK%V(C3;1WFzhZr`CXrUVAa^}vuw5qpI^&zLKeY$lJX*8R8=OJJdSfHDXGUv$ z6b&!v=}UZHNUa<0%RG~7I&^+MAm4N~H|?^rIAO$YtG#2TeKm7%)2GN+oG+F0<1jr_I_%paqgCd{f zQxgUvwRmT}vObTL2LG}}`95s6$SbF%=`l2#Qb_~3;8vraa&hl32o{m|Hw=$)b3wV3 zBCQ%qx4#Gb*c?Pn)p%J&lI^HY}AFvkHG{soDS9PQx zXw20fwrp#pyPWu-l(RX}PrBKE44VIVXCH7)VRvx;`qsrpT_MDoKv1dp)9!0CW9~P} zvg;P{>p=~;U=1gw1{5kcJZo6W&R(!;f>+rl5vrA`{M3Ceow`RL_{1!k#q>Lrb!#R@ zXfl=$F<9Al%h!tq6$)~4>IaW@f4zhNMN_(vR}Hl8^ZaZ ziJ-lja`LWdVXz+k=QUvA79FG;PSUeqy?*`@fo~(wnBU`gNgK0mjjYfs^t#3}oR_Xt z!c_YY{zgg7Q)?`Cp*31fuzIS%VlwIK@O_cyOE~;5K&s=+Agmb_8fcJt&;a+e8q$u~ zS{LCFRmEF>c;2x&0B3U7lL=>m-U^nL>43e6y{?Bwk2e0Co9IJrj}EvLO}!}n)Nh`% zvxDxN=;bDh?Zj_>-;SrjU0fjR1rYax2hoFig<`l+a@Yi?$?Tw$_|(J&6k?jn_JM?68p^mDtg*nl&`>iRz ztKN}D8SB1$vwU6e-X$t7wA7XWa>y%Xni$W&08bM_mFA?2KX58n^~9W_55r0!0p|Bt zr0U#V$cBKZ$^gE$7UP^Yp5!7H`fV<&lsz5No>iyap}9*-H^O=J4>dAYMlx~60b3P* z#kn)w8^Mq5Qq+F0Jfo^jMLDlqKHtN#HVvA02?osMPu$WUN++nSKj8_^p!gdLIX3^! zOf|urEue7LUP{cR^@{uYsdLI(Uu_<4u9%?Ep8WQ$6Zbg8;nKE7F1rEC`asqvP5Xm( z>4f;tovH4-_>4D2*DKFjpSm|yjLXJyj~8*SET%4$@G5jYH?PHraOh1s z@OGL6G?uv9igE|Ng;T zml9^l0~uERWae*_F3bsO!vY#V59z4!6*)X>Hx|6Tk`+u3ir7>CkU(lSi zyArzB%{i*bo_-TOG|$a3*JKLheG$U}>b0lUK=`C)Pk=sQ!)=FMXS;+3ufkNvsZw54 zKbmg@n+h3M_>jgG&Lw1ZR%A|eeqA!R40m!Bqan(DQPFqxakvD6G=d%sw)Ql5B@gZU zlLyy7A?aOSAK=`J{%y6DZaEDe1B)4P4S^a<^~8>j9dgbIcm97`x`0O(y9xn&vicd@6jz zfp}lSVv0+Apj_NKO>8a{{MIS=Q(c{ zihzqrck5DugP^|vwJnby8t9|toOL456UW*|eu-Trm-d*(uRI-#n!nKfq8GgR5h>>FTiCPQ9owr9DlRk)g66&$XC#!a86 zq=FXsUQIYj#WcP!`Y~lJ!zOx@!0W1CQ_VYlpH9S+O@SL#gJ2>EY3b^GCa~kEXRzNQ z9xT+h_KnU+M(b#>W${DMlEAE%SzJq#-FbGz$OWS9{Yw2mUE~V0;C#$i&h)N^6RS`? ziZd05nV=JUZY*G_52p%=m^N8gMp}w#nx#!uMspKUcC*puuG5=rq5QlFn*Xnpv;J$s zkJ>(>bf>fo7!4AFlprai8QqT2FmN;|sl@2f9nuU&r-;O0FhZKqN=o?^kWfT#p65Tf z@BiTQ!+Cwqbm6(u-7CyeEq7Lbd2+Qv(miv(C)9Q?4nap<{)50ScUT z2*nh`Ay}gREBVx`5^K{!5o{{hv_u(R=qo?0lXz>8)0~G+H2W@NS0`_iP$krY^vKID zpCB~x*1j}n1vViPodvaQ_%Hb&?Ts5D3KJibm?d*h`&#^g2O2yb;2|;)_=Gd%f^Mm>f3w{^gE`O{ z3q+=pMv;cbwy0p3@7r9wnZz%7$UYI#wP5I(j^vfr$jxKhw!EkJ_#W;FX}j!wR*m(b zAW~-s2gT&`}s@Kcn7L?iRnS;KUS1*MNo~O0M4IJqO z)Gs)076CmL1bBn%Q$EEM?$ukim_231!|P8R@O@funEjHx3Z${svz2=BB{r;?g>|}e zgXJM3(g0=K)9g3ZO#Zc!Jko!w30mdt2x#l=V<(NH<6x6WdZKwQ}43p=5KLsx0`YSj1PI(*yHYjaXjWCW?0J`9lwL zh{lI$>QVFDATx`Z*A)y9vmRaU%`S|1y?CE?{pla;%7wayM>8G;Bi}U>s_GDIm%s^P zCYOwm65<-FWK!)j$6TmzMs%(xVSs#-OnaU9*0OTzEb)2Ry04Fzs2aa{W7su%*#gYt`B_U7CC^?tZtet8bSCm zDD|Z6lREnA?WgwPui=@GuLLepa-Y8&Zl9;T?%D|~xGI8IEgMKQ24Lx9=aQ~Z z3%e>Iaw8F3(uLQbOR(pojhu8*NrPM@8l_I%OUQb^Ta7V~mY($9vPt z4Ml3FC-gY1^;BXuzHNR-%;buCo((00Koo@B4fpJ|heATIjw;e%S!P{V<$i=%_~qB2 z<`1%!Zsn#O17`$1*p-24QV-m5cjXrw(N_P~EH}b>depw~me+YM+S2ubQyXZ`neRy^ z1-0o=H1X#KccEV0CGsOE`m&3C`03t&6aRc8!Epi0(ruX8Yljsl9XgO5{? zf5CsT3RAY=xL{JRa(G*f=wBs#$O7fGEqFkqgIeKZvo3#WJ$mrezTLNfy(#{K6W6dN z6mWFoY-?lu;IZ-Zk8Zk%GVTRDd5UW7Li(oQNM*xuJp0BfNC>*D(Y|2x!Dg$;d$_s2 ziWJSi)-cwPJhM2eK@BS64(yU6-*)8^t#hIH?e$x|;bx?iB~P67_yK1pbY1)9f<-`9 zjP8kr$?@;n9?QF&3!A6<&nLt6cTNzcq=9V*xQkz_Z^ok=M!j(?f`4F}LRdBi*DmvrNKTX&~7NiV*VTa@gTHy(JJzxr*QpIy4U zdE~(dM!ARwg3S_+M<|ygW5{m<=Bp9SK2;+w4HU9ziQ@Wuyh83a)R^UZZx}q__^pjd zyl=2=V_?m~k<1d0rg7U-T4WTt#SAz^QinFMf?Z_=$=0{(o#_BKMK>ulwrzV?!OC## zOcZUrq6_$*-!5?sl6k!GnC2J0H>@(Yd2juqEzT-^PKxC)JqOR<<(g*|SoVmQJT>&BaahC;x$XM56_ z&{eQ$*00YeV6&LuDe0+}s@Ejplr?*jK}65@q&#+n@i8%OdU)xqC)Wu)gFuXp+SkK< zNj&?-yDI9nY-ryMM`U@})hcXzKdjp4=0q`qE39i@|G^q42^Tip%2c!Cl&YID<|?LC zbEpH}_P&##|3NIl#z`o-Kil$C@Zvy^v_Vc?k+4jqwp}-O1AutL-J+ycY}XQ;k@Q#k zZaO-@KHtxL_4|XqYjb-GzJ7t-R4fLnUfI`8yXI+Zk}js^B@B}nvx9g<7JCYsa&3U( zeN&zEIb0jI@Kxv{wDkaHbvJ@w_oozu?rzN<0{ZzNW z`PcWh({*mBSuxWRgamNPMD3219}-H<=3Ya?O%uL>Q-W1e7#s5)@$y(@zZRjxa`&>rSa4kR?DGx}t; zMWcwVsqBa&RA?x7-0vSjpUMTcNo=Kk)=uaZtf4~Koy z``WN|>RR|9#&U5bp}@SH z7n_b53C0qiMx|}W^pg<3`gVj6)9YK2+JKG?p?qx+ovfS62-10<~6?jvQ8xrItxxA&o@8y;5ViG%|)< z$#b>U|3juiQ0vKyaRGK2ug3DN+YPGqnyFhqxRNM2owgn^zzO|>o2~Q~>-8tnc1)M{ z7l2%>uD^cJ6S|Y|mG#1W@2Vuw#Bi%^MEAdk@X!c~8vhNY)mL0!hxWywDU;?_FcU@P z~4>BxFJiDvrfSQS!6(7o(xkeZ%pkJ|}1nx`EZpLzNG zhmGvLOjD{q<9>tX{5*;M5_7p1j(T@aTtFMo_tk=!T`Ho}kVi(<{IFR%}{dy+rO}ktDx8xP@9iP37_l z!^Vi2v()KHB6*^nyo8P;+_<7(T-e~sm}WXI>^66lJV{UGVdx<>bxdT_L&1_&QC&E9 zJ}R!d1|phQH)FJjUcR**PH6hCb1^t=Ab!Z|bJYO-EVxSYh3INgF(a3g&?|duz>|sK zed`d(bw6Qi97D2#LV9XE&MY=Cb4%0~YkDxVcTR0?#{TB;tN2(HFUwD=+v$B=qgM81M)UH>x+$QUl^-;U$iP37f z0gmXy?vW>yuCzo#=#1{|fcrtrtF=sX>^&O(UD~hQQd`JC&2{$d?S@X}R0hI(8CmRE z0wD}7#z$Fh0b#2xHq;JnF;d&1IbDlS1leLR%x+s%{=4jsxwWM*PqH(^yN5Q z2syJUy;XSRap0#OmGRw(8P`(b9EG2Y&33jxNymAL7==H{A3pXk3AReJ=i6PBMOngZ zN^b~=rsT!AtvtyD<@RXE7(jFw9u`U?73Xo{!Dn`PCr@(^qVwn~#|0izi>7|ShGsgrm9BSZpV9kjPuycI~2NV1e zBbON=1h;S7o=JMlQLlc}S~ZU2{pq$eTN+1P$~rkH!Qs+^%jlnBh&X(w2okHQan&=bG$)t5~uT4I!1ra`yIJmWV-<}p2B zmQ?}UZCX1)reY~NSELu7X`vy3#PrQlJ&<(dkh%R?E=p@4jxAQr6n0&_bR2Bwg zcG9{3(ycHpF;*vH(kZ8Dl~o&S+aRPz!2D!4$sy_QrrO$+WRhCK$uOPI zbnYYY^4_P~$)(kD5D7TJ6h6DC57vdJ5=A%D$USlAnLRbNXhI*42(iF)9ACe+Gd0y)!1 zBOsD|pB{<6^mk34uiDuYkJ$6eQ{J!&WoSNUt-yv*S6cnNqDd{tzAx0t`+?7c5NY9I7e`~}OT={mY1uSXY^qlndA3^@ z=Ge!tHEv55#BKd@qO?QC!nlQdu`uD@dD=En>7zCcOFRZsn32{n`ujx_6YNqLo*&6x z(>!FAG$7ABO-b;|tbierN<~9}$Qk%KD!5Xge&=;58Pdv z8famA_H0RHrXUA}XG>cvf@y?~g_PA-CWCvd5&sB$t&i*0f!sq}b#n=AzZVbI8*-nw z459Sq>Y;Rw=C3mJp+{}Zd~NtV6Hp|z>IG;9)t<Q@4H_uh(@xoqc{X z`j+419b4?8bjiv*C`4T^^mLoXAH(E@qe)xf9ELKGQ9va`^ zkIVxB{(A_%&+@WTNpwS0q+%ki*vA#A_?a9optfc1C zN-?s=wc|{X-6G1$_64E4r&4{6--9p*O zv`Ps%QCj*h3c6uHjNDov&&akNZgD-i;}MO(94@zCxvug!md}{%i5%;}RZn@tMpA2V zuk&Uxjy9<;Jj<5UqCNONct>Wn!T~5K@70K8& zi*2$7dc~P)sA`)2fQ`t(U;wJXV?l-yRF`a6LD84DczO0>6SL_Kt_igM^~Of?f$@7| zjltmQ+&!)G$6N3k@BFa6EB`X{GwmpH#w83M(at6k^`#(@l9lY9v)mX-0ws41Gr({7 z<(DW)(V->57;~#p-uN*@+d_oala$sD*>jPxh%rZ@c1BnX+xOaPX|l1!`25j_i!^YN z`g3ew1-<-|UQPTsF2mwkq6t;kRVkw(FLc_Wew4Oj05rAM53^8ijh2gj%jYBzoTix? z&Sz!+6Ed?qc00vS(NgHVL_5N&j@C|p_pz{6Nat0FtMU*?H`wNHiFqjT(J=C7i9F{& zf}y^5m9KsJHf%?0K5K=Em;-*7jQ9OlrYl2ubgwbq01E0)&KT9!E%F#>fkg{NJ%g0qlTp|o+mn$d zk5;=5^t0r9Xr{0LXsvQrsm*Y@!6dQAj6*#&d|a5P(>c+SKxZ&io>fAVgf55}a#)J@?kylvTfkHCZ5vvV6t=acbcsomfQQThDcB`ibbv!076P4D& z5oKcZpc$Fs^SgG{MYPI=(Zn(`q_qVgKpcBk40`fWAIYbr=H>sU>&L>G2`h2sWU4S@ zS68@0deYZmNTcBhL~=wkws1&?(_ZrQp@4|BeSn8Ugc39*$(vC|Hmm8Uz;w>7rHg^1 zjth7tBNM1I=iZ6dRrH5Eg?%#mg~Obzxm|0Z>kRGvI# z*`_Vr44z~RTc~R@LX#tHE#g4~as(Zg0Crlm!|F4pDLrw&=h_{|)$_MQ^g($^3^3%4~bbwD{lxC;5lSS4{rlr^Mv&doaSnZ8d%xY;oFSjXM#Vj*<1^pF| z!bP{a^&f#^vDmFOwPHGXnbD3HyiyOwKEnd=)H(a^ZI`=mZT=JH{^eH^VKp(set6@G zj;k4MvZ^S!6Y}hiRYXRgGGDzNf8-}d#5C~M)|gBj8zC;HUnb_8^N*lvoqx0Kw&VK? zPFK!a&%;N7-+0BuL=7a{(X(nR!ZoG62^Y-YoL^`58{v*BdVYy!TIQqnA?N19{J_SB zU~rDu!fv&;^LHcoeW@loh5wo0iSPO&@+@`&yhsjwZpWD1Vi$9e=)xY1r1rea+3;~ZdTOGL9;n+S zF7R=+Yd40(zKul)-otVF%9IX31yh|0j?J8@iiK3EhkrG&j5Qe!L5!#<3q*c$2wtXQ zw+Ly1b639J~m(fXTj3H7t-@sTqeaLd( zd@oKpx|}FCg;|~e4jO&Lk5Qz+S$-GK)1HkTGf-mw^vIH7ovD21=#2g`#N*2@fC%98 z9!)V4VJA7+zj^hxcat z=~B|P#*)2cFa8nmK#7SSUD4A=v!+I~A`rQ`=jJ~ZPZmqQ9nGQseBLq0is7$kCwGqUvq;vq3TC&LGtv zm-J@q9M4NV=m39el^ zR^DI9+4PrZ_2kx0`!Ff~kf=Cn#e>yl`xncy2+e94{P+We-ru+Sxtn3&aB{VdAYM`S zSHBVgK`7UqCo_+v9ND5TyqLlp9E$0^B+mzi(F>Y{k7OT&JbDp^b*VEw!UDydM%Jpm zHQ*gX5-x-8xf3uRm!S+PR)l=yuJmG`BFX+av%dZvOJwZ*xH3(In14sxFm!@Io7!22 zCn}d7C>ZaQP5*=|XAy;<%4t7-ILo`g6xs*-a=mM!3qnc->T8+I*lm#jCeg22k3{9ZuZzr#>MVV(E zGJBS1dlqJw%+h*nT6np=Do_BXFQeM2*<&%P3FUD=l(2#sux$Otq}CZAiR;k#8^}=~=Xs zeTI|Ve#>6w7z4SEla7w7Zyr5JV=O{i%IpX^F)@Z?5E!qK`yH}2q%sb0afSaWy)Hbh zPe9T`X?=nA?Ey_r6*fXP?8F0a&zh+YKXP?Ro03#(q@TuRf@pjj z$D$!e+`RN$grvZ4{dJ?w|50m4HTBczq6aAGWtF9S8qA6o;dXXrxU*q7ZG{UsN>QZN z@!NIXSdA7|25{vuwfJ$jSC-Mrr`6isPy-g8BqOviwp+ektZv1?Kx|t&kY@AO$aCX) zcZ5pH`Dx##illPL)kLF*!&4SR&B_gvBvbE1k5=!k4Lu27NDOLm&n2>-M8Z;kodbtQ$8**chnb^4%8B_FZ;mWzT^t3c;@5D{7}Rr z^SEO4Kbggtw^bAN4W>6@y=#f>!Q`oPNqY4f4VE25ZsJ6rTAkzxo9X>I5fP3`a!cFE z<3@g-@d~daS<|0UqcNwSfl0`1T<~e+>b4sl-Mx4Of+dR9G6~kEX3XyOuAeo3tlK4L zMW8YfY+V(vCj)X*BEUVcQfULEkIH%FvHI-{rI%)fmw)h$44-QrwRC$*;XStgaAbHc znE)W%ZMXh2GLv1CJI|_Yl2|~5XYfVg@^tgI3PFJ}2j^4K_nuN`&x?3#E&k?2-6(p9 z0jAt|n@*ptM@SBXj*0?g5)|o+Z|HKIn!xwHRJ^P+^D3&E;$G-duM|5UHDU!eKF70e z@yJA~DQjp|v(`&~&OA#+**@<zyp|4)`>(`1LXX^-XWE=b#LRy+#PYaBsy;aBARFY? z@YQ%SfZhH1kHDt&Ih{r|C!TG61wAj;<<_ZSmcVX)2<+-D@B8U-SPwXs>GYsGx-?-7 zZu2DaS~p}w2oYT8Ah{I3wg2&(S`1go3#V;$%O9fq+?IiSHO`U3RH;fhTe!5^(~`1V^~ z2Zm&?T+#w5USqPAq$H*8naJl-t>*~4`u`Ykh%g4aeGLviZseerW@?;eXs4CDzlgu% zZZH}5)NZLy$N*N}Gz~ERy|0q8^tp9-g9W!)=`bqlew!0ilVQ)6QuiUZqVce1d+M#9yV$n2$s)YI>By5(A`Y{CD|%$lpgQKfYj;Nls(Q?qS}% z(Lj`7Pw6#z+a^BeB|gg$DMN}>Kh0SNc$gRCvnR$fa~yVz87QoncPgT%Yai6UWUM05 zL2Xf}N(gv)eKVlKn~T*F*mED|h#2`?>ZDog)TbRKERL~qIeXE@u~l@&=CIYBzNHy( z_Wnfos)EV_1sqr@5b?nuo-7>8(tT;i?-jJ~;Z71dHPW@7h$X=390%wX<^nmIj9sy* zth2@g<|0EBv|&c&gHg_1`yb=lwEhv$a*%A1B*A0;z_RG zHM^88BmOf=+y8sN>$~TA<~q;0fA@0k=bZE0_j9bxXlS{pC=^vLF7MhV$1Fx}Q!aY) zg+icUga9lIjKv}m5HJCOK?8l!P-zSpkHAY~urM?f10kYdXo55W=1;KxO&MAK2eFe| z`WUn)0#3y3slm$u{~gUhMq48w1R@TG{|=)k`$O^YhemRPiF`pYXadE&KMGmkV1r4Q zVGkK8EBEg^;)B@et`vEjk9d{unf4AeR}5kHS5&IDFkYYc>(epew9S8TZF7nAGJ82* z{xj*EQ1?wlYmy6X&w_-Gxor|QRqE26wZA_q-FFK~D^{4g@gzFez9pZfFo7rJaCt#= zqu*hWQ(Ful+QWp=)58p6FV|l&E~Mrg6cAexB%8X_tX~6DS5F#rXS)xT4~8E)Udd@O zS2%f)$>i88JH@02^1|la&_?m-sb$atZ89(xP+JoxTRI=;F!KDyODw_9FDkQ^*F4DO zEMm->;fZ;$VZw7t=ylZ~c$JyNN-; z#5Fq78r8Y%h~6~k+lR1$JeJWp!EN?F5{QejG620A)cY#Tw&-Ec%h;=p4TsX93ag*z z#;#A_17O^DWK||#oe@rdUUg#8Jzl!u)+{F)i!lcKNB(t7(wvXmQ+4CTNcwd*<$VDhG(BZfg| z%!1@K?=X7zlbIgg3%eToc81L$AulR0O5I2FZ#H?+)amZR`m25p0>_J?=f#pDWNyCh zellyekag(Lmt$)|)sM__O>Uc{`mxF=IF>qJ&RU}nPG-*}Tstr=_8M$bdE&9oVnxxr z3Y*S{ZVHZ*xFu=RR@YOT0y9>ANQ0L*uy%}#vNk(JKg_T1|L_166)Pio{_m}&|1|B% zOe#Df013lWX8n}YK>UCbd+@R&y~KOv{siUZ>hvK%qloyms?*8=tX|RNsgbK=`*3nz zy*1Y0S%PCH+L;CwCOUnTHA)^Hsb2(iU5eG{vlZe8Vje*`qNt_pvMO-FoFV?ZYW1vc z!y%t9l{rWj#Kzqr>Y8zz5IYK)-En{M8v1j-%XPsDr@Gw8&%xb#MOWrED=KwiRSyU= ztg5Vdt=N}cGUIwRGp;YM4yqagV_Ap8FDsfSd`a-Fw%e_0D7qqgy5fJSA(ITfwfusVvG&>h_%S017V&5eRxp!KR zw?vbO75N@j=dTEh*(43Dc^M*mX-Z9$4dSLF4+=(YNE{IyE`FZv%wElZrE_id82wu4 za3iV?5L0npXuZR}(EaTUN}x(R8$X;9!)boW8xQx{hI)qt%?zF0w3Im=amcFckzK-_ ztL*_j9~7ET+UXb&Vij0uob}?LzAGuVImNn$IBhEcQ+P48$kbZmsUp+VW28J5R5?ax z@`%cK5&Ju6deKF&aF? zAkL5GOtIO#xTxKNA@FW9q&;o1AYwIxN0_8+c%`2Qie;s^x2r62^O#8cS=+fehFe zg+{auFK=FX6Eh*u%zo1bog-4F#L{2IZ>Gy|)Yw+{aZ;kYz0Lq*&aOpDtg+PJUd>PA zJSrYdKN^XXf7MSPC1a50$Ppli^rbOsu=2GtV?>L~76q={_;57-)Jbn~{3*0#!h-yn zwC559?Ks=g^Occ8PIF;5dJ8yoqS*Y5)LQ}$-vHaz_rFti2unwjd7wkM_* zy%!%0FfIpl|(&suJbmm1L+FW1#-V5tkwnerAK3pbZ*itLJIGV4^mM zyYckIV@(5pHuSDathRK3FP8CEu7j$oft2vaC*dw(VbjNi=05Z%cNYQ<#2?5}JIW;c zZp|0jne-@#ZdI?7*&wf$#KRg})^C)ZzTk=j1!O%5=T_p!e6h-Z6u5Ckm&%UrOQrZ3 zzwBs(oNUN*NEmtxYByAEAULxmC3M_!*K)c|3I8G`z}cf)xmD9$hHb*~xIpq5MAzgF zRc2?`4xK>VIS<0+&NDVnQSi~@s<2kB2|~m;rutZbFW?#Bi&U%<%^Fo_=qMFvlX)8eAcI0 zk+L2M^%+r9jmIgkMsBY{a<$tDsLh(Nt-)T^!5)7mL@En^#}*4=uIEGvUGOtz-_2^ot?E!f~=aQJp z*pNI{yc3Is^JUV;3bHYtf7K0H>$$_#@S)m0HE3r`>FUae*5#`!Z$E2|C3L(KV*ix7 zajdgFIxyfPcHJ{HM*iJa<9IZWF>$-*0zrL6_x%KXa=o%w_kMmHV?2z7veW@~S zTiM;a3vZ2ZwS_DvH<#{(nt#m<48Ife9;soWeMWuGQt2D8iIi$&~Qy*dyt`&im7u7t3!$lvb6Nl~WXP zM4;v60KYTcusDo23_<`ZsmP!xAUz@i36)Y&RZ!Ld{-IU_3Q;2<2uPR~$O{JWAtE5> z0UkJv9~$6^@dtPlQCK_xLv|BDAhVI+zyJUg1J?llkfO-ri5?VlT1EjV00arf;{l#9 zFu8Ie&jbMY7j0lP^mi5eg5So9KqCkUFj5MFBTJJM1ruOeQXr00sOXv@59YY2RmlbLTuPzz>6iYFUHPa2y!*-S~d*?-+8B|0{nl*n5xchdkLB zlB}5shQqXAXeoQUJx70ZebWzvl4FU6OF;r43@w<^fx_`$)zt^Q&)*v|x8M4|v zyZa(PxZi|`co$kh0&!v0xmS{FJd? z``~>BWeV)~seT7TyvS$>fgHK-PAKe+?fb5vM)5~WWB=~QDE7Mly%yplZ`2?!BFX~| zMj-J(!hTa4i-ylLf5d;7N*MQWcOP9_#WEat6`}LsVmOPj(=!DlSs9rp0re`8pyvTW ziUW?S*IUMN`*jai+>7uyb%tP|qMveG?uJr1ntn81rTPAsGN*~ibgO>l3}pJb?d9j= zq*%BUDa-99{~Gx7VAJv%+lA#D*2dm5?jVo(=BFyLw2_7@=KRS zUT5zXPy5d9ayvDlhboKD-u^r>F|jt$+m)?sg(R&nW;baCZ>{#U{~du#U!IOmPEPi7 zn^x-UTG<@#{|6pBy}GM)VQY2Ph>k~Xs$qHO3;&9%NRX=!c&trb)ic@6>4EaD@8_=@ z?Ir(YZE%Y&D{f2MN^g9TCVl8sbo$%14Cqx@wA-|~*3PtopVE5U!8PpWCcQ?g$@o-3 z8kv)jknkPQZV1}l32lYP`rX{&^zq90;ZY==@)Q6-Zr*MEOf%#^AXMxZRry}GsftQ&^Yh&^=qj5}Lc5VVoMpHXbSv0gc5oORg1CTT36S|aa7TzS|=GR!qp zjZm!)&)W$-cb z5R^Fu)H5xC`Snd#Q-?)I=q+jlqaKp_3$ntN+9lt1(|gFjZa9iW_3I?2q8MK!-sR7G z*H|~Lrqv`7hGZC+8R=+`mpbE!|zH3E(F7;GlQ2&T0CCmyev4I(7w~*MYZH;C4C8V{prn!4A^e zrF2Eptd36Iou%Ddq3)4l3J=cj!4=+;uo4NW>ve`oqbswU7`YgK_^Vk4>k|o3@B!=7 zr@lHX=?ima0m}Tx!y* z@5?{3VX;Nb%;qc(y5kXi2#xXac~yR5L-KT^GeG-ee(0%OFfXFKVtFFK7)8G%szY6FT*#@*+g_dWQi4 z?xE@0{%G>r`|jI8F&7da#~Qn;gngxdV^)xorYmytf^~G@-9o>zF5RoR3ukSHLpE0m ztv<+QmtFWgwDQkHsM?a?v5oSH@#IkTrO-@uQdZCaenVrU^uzGRprFQvQPsIY5(=|hFjBu*yR=n83YiZZ37o0jS#>@XG9NwSOL+TKzLfBWkeV^tpWQUq+>V_*p}5S|D!|LZ2$DGF4|+^=;r(KN$A&sJPAvm! z=c<5YknrlfTq2h)}w_+5U_N!yG$g8A{`ke$WfqAu8vvSO);0SS*+G}|kIi~SK=f-Zf z%B41kgfn*+$Mc+HJ<0X)fzDNS$-)f=oQUAW)lI{3ef^~pyZX{GKqyprmV`EcPbDYhV z*s3-|qyD~Gk%-X48*X zel!BbM?k%0>`Ux76LruhQ=dR+>s8SN_?f)3o?Y znooSO;b%==W;KQt9aQxgE;1SW*zvg#@flOORV8X7uQ6eEJ8P1!E$e2o!Q^oA!=hCs+PE1z`9-HaO@>D6Vlz8;}8&>ylhJd}u*8*q$Y z%vq~xyP+L(=p~g!rn98o!%o*kxK{eG#S(9Ezj2bK)FR&G3T$U9Et<(1BF^ziX!W^NXV~wBQ zUuL}a2Gcm#mNHx+f4`hHK^)1(OQVA(>O~#2ElqDboA*~Fz%L~>FZq5IJ8jVThmq4* zX@mmGPP%S8Ih9R(oJ#l%*jp0G|9)bkd|=ylJR7Yj7+G3!tNT(@(z^&@$a3o&6&7L- z@sey^7Ax!aOdq51F0~r-vzl95hN#TsH_5pzIOOL2mIF2}+jdo)0o7@*1u0>t_Siru zkl%xHw3M(@AK;?;_k$I3?0=#CW0pUqet}R?PEdZ51-beE&r^Q4`4de=MLALVNkQc1 zFXt=Y#P&Zm|N0S{a>DYHa>>nK&Rh1q-530|jHY~}{z<7czkadam!t6iQ<42sj-70u zk^oXl?tQ6!Yrke4lq~Qk6_7*qZ#m(gT7F$Blnmu3S#kWHuly(izzrJpp)`!5U2 V+KlcX1;#-Bag&oJM}ErP{{iH=vzY(@ literal 0 HcmV?d00001 diff --git a/source/test-resources/quick/quick.pdf b/source/test-resources/quick/quick.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f7a1883f05fe5d1baabc8e28962b06b133777689 GIT binary patch literal 18638 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≺ND&( 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 KWy^AUV)_LI7Z*4H literal 0 HcmV?d00001 diff --git a/source/test-resources/quick/quick.png b/source/test-resources/quick/quick.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1f89b8fc0bdb70dcf6887e17b2d83389697c76 GIT binary patch literal 28540 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 literal 0 HcmV?d00001 diff --git a/source/test-resources/quick/quick.txt b/source/test-resources/quick/quick.txt new file mode 100644 index 0000000000..ff3bb63948 --- /dev/null +++ b/source/test-resources/quick/quick.txt @@ -0,0 +1 @@ +The quick brown fox jumps over the lazy dog \ No newline at end of file diff --git a/source/test-resources/quick/quick.xls b/source/test-resources/quick/quick.xls new file mode 100644 index 0000000000000000000000000000000000000000..e6ec64cb4d52d4b2cd5b418f5a003fd18d912d43 GIT binary patch literal 13824 zcmeHOYiv|S6h3#m+ucI#i-!fFUfLpkBGdX%13LK31I{ zl(SM6%2$vdf9552c7%Y{APMP`u*BrXmc9;aQ`;@_7fL|ph!h_yE~zRhSy9_s?Oqo7 z7h{ZKMZrN!07C~K>(Z>uiyS4p`nqvE7ktLK{h6#2DMC1^OWAvlm`%i&C#tE-zdc&}8- z-5!IkcGAA9s%E`Yk84>iM%O`Qpz2iQiO`-Em=G%L}T81 zK(nf4^0pMGOQqG86kR39r3QCHD=vcNwMnwIOdFzgfdEmPsz08!B&ShJe-;r zgtJDKoUuZP7;UF5xgziADkc|#l_Fa^qxKU6U6F2kS0esokKGa9XFoo$r?=0J?~Nwx zeiX;T`v>jz`0fmq4v45ca@7DAI-pd`FG4*qp$USzAfSy0Y)fx6(iM)`kijS2peeAA^vT{_>^3cZSmC3vY{x%>sj$=9t$rx5*n1p2qZn+zIBs33^H|vCX z$@~xjaAx(qlv#bY&lz2mqs$>F@5IGzItykaZI@iV&eypx4@R$?`0d+GyPE6TlwYCz zN==_r&JbkWuoSI_8BCr<$r(Rw(WU2A`hccK`M`sOr9Vo$VYJS>~AhAXQtcb>P(fm%eLEKO-1`vl+DaxD*vSq);MLsGV%aJCi zj3BW_8hy=H&=(*S4UMcUOVwv5E6V}`x!PSXNS>SmRPCtxX~bIDBiJ(FZu z42_Izh0cO1t}=ds-GHouz)U(dLEjAz?!hp*iJdwlgN`6fcIseSr*;Icr&AMRhFhT% zUO>nE2)@ikb81tnXZ4yoi8iHn4)lruem-AnQ+j00r9r?TaMKVN!^1tGJK$6oIJ=D= zeC_wk4|nDbzZ{avWgmWzy`NN}fDbN~HzRT`+>Xdc+6#!BoxVWi-1Q@30V0>|T)JdK ziPKFXa_nZM6gl;Oh7wjsSf^dra!hrqq4U7t9y<~X_x0Hw(QyAjqN``O9ri5e!aeO1 zYd8^G_2|du=-#d#dviP%i}woj=mYuQgASL^wa2EeNFv@B@94MJ?~6oZ&g}Kdr3Gc5 zd}`IBTuOsYd8fa$oX0yECKv<^0tNwtfI+|@U=T0}7z7Lg1_6VBLEzRv;9C2C-}$rW zk1W3<@9?YG|5sgpm;3*7h~E7_4fae8l2yBr!e*O3=V!iYr zPskuzZsuV=z8-P!jQz+CIR`5vP8A!bs)_x<>20y56Fal`Y~fg z9#A0!q(uf~4}M|2_l-cILwzErAOHM_gEM_7gME%6j-Q#@*KcMg>^H$eM16>A3tXQ+ NtN+3MtLdcuzW^T-=fVI0 literal 0 HcmV?d00001 diff --git a/source/test-resources/quick/quick.xml b/source/test-resources/quick/quick.xml new file mode 100644 index 0000000000..3c9c602a99 --- /dev/null +++ b/source/test-resources/quick/quick.xml @@ -0,0 +1,5 @@ + + + + The quick brown fox jumps over the lazy dog + \ No newline at end of file diff --git a/source/test-resources/quick/readme.txt b/source/test-resources/quick/readme.txt new file mode 100644 index 0000000000..73f7a002c4 --- /dev/null +++ b/source/test-resources/quick/readme.txt @@ -0,0 +1,3 @@ +Files in here contain the well-known phrase "The quick brown fox jumps over the lazy dog". + +They can be used, amongst other things, to check that document conversions are working as expected. \ No newline at end of file diff --git a/source/test-resources/testQueryRegister.xml b/source/test-resources/testQueryRegister.xml new file mode 100644 index 0000000000..1b8f12a1d5 --- /dev/null +++ b/source/test-resources/testQueryRegister.xml @@ -0,0 +1,67 @@ + + + + test-query-register + + + alf + http://www.alfresco.org + + + tulip + http://www.trees.tulip/barking/woof + + + + + alf:query-parameter-name + alf:string + A name + + + + + alf:query1 + lucene + + alf:query-parameter-name + + + alf:banana + alf:name-of-property + + +QNAME:$alf:query-parameter-name + + + + + alf:test1 + lucene + TEXT:fox + + + + alf:test2 + lucene + + alf:banana + alf:string + fox + + TEXT:${alf:banana} + + + + alf:test3 + lucene + + alf:banana + alf:string + fox + + PATH:"${alf:banana}" + + + + + diff --git a/source/web/WEB-INF/web.xml b/source/web/WEB-INF/web.xml new file mode 100644 index 0000000000..320a759feb --- /dev/null +++ b/source/web/WEB-INF/web.xml @@ -0,0 +1,9 @@ + + + + + + Repository +

    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 + + + categories + + + + + General + + + Software Document Classification + + + Software Descriptions + + + Main Software Descriptions + + + Short System Description + + + Requirement Description + + + Architecture Description + + + Implementation Description + + + Configuration Description + + + + + Software Description Appendices + + + Terminology Description + + + Internal Message Description + + + External Message Description + + + Record Description + + + User Interface Description + + + Process Description + + + Initialization Description + + + + + + + Utilization Documents + + + User's Manual + + + Operator's Manual + + + Installation Manual + + + Service Manual + + + User's Help + + + Operator's Help + + + Installations Help + + + Service Help + + + + + Development Plans + + + Responsibility Plan + + + Work Breakdown Plan + + + Schedule Plan + + + Expense Plan + + + Phase Plan + + + Risk Plan + + + Test Plan + + + Acceptance Plan + + + Manual Plan + + + Method Plan + + + Quality Plan + + + Documentation Plan + + + Version Control Plan + + + + + Quality Documents + + + Change Request + + + Analysis Request + + + Information Request + + + Reader's Report + + + Review Report + + + Inspection Report + + + Test Report + + + Review Call + + + Inspection Call + + + Test Call + + + + + Administrative Documents + + + Preliminary Contract + + + Development Contract + + + Extended Contract + + + Maintenance Contract + + + Contract Review Minutes + + + Project Meeting Minutes + + + + + + + + + + Languages + + + English + + + British English + + + American English + + + Australian English + + + Canadian English + + + Indian English + + + + + French + + + French French + + + Canadian French + + + + + German + + + German German + + + Austrian German + + + Swiss German + + + + + Spanish + + + Spanish + + + Mexican Spanish + + + American Spanish + + + + + + + + + + Regions + + + AFRICA + + + Eastern Africa + + + Burundi + + + Comoros + + + Djibouti + + + Eritrea + + + Ethiopia + + + Kenya + + + Madagascar + + + Malawi + + + Mauritius + + + Mozambique + + + Reunion + + + Rwanda + + + Seychelles + + + Somalia + + + Uganda + + + United Rep. of Tanzania + + + Zambia + + + Zimbabwe + + + + + Middle Africa + + + Angola + + + Cameroon + + + Central African Republic + + + Chad + + + Congo + + + Dem. Republic of the Congo + + + Equatorial Guinea + + + Gabon + + + Sao Tome and Principe + + + + + Northern Africa + + + Algeria + + + Egypt + + + Libyan Arab Jamahiriya + + + Morocco + + + Sudan + + + Tunisia + + + Western Sahara + + + + + Southern Africa + + + Botswana + + + Lesotho + + + Namibia + + + South Africa + + + Swaziland + + + + + Western Africa + + + Benin + + + Burkina Faso + + + Cape Verde + + + Cote d'Ivoire + + + Gambia + + + Ghana + + + Guinea + + + Guinea-Bissau + + + Liberia + + + Mali + + + Mauritania + + + Niger + + + Nigeria + + + Saint Helena + + + Senegal + + + Sierra Leone + + + Togo + + + + + + + ASIA + + + Eastern Asia + + + China + + + Dem. People's Rep. of Korea + + + Hong Kong SAR + + + Japan + + + Macao, China + + + Mongolia + + + Republic of Korea + + + + + South-central Asia + + + Afghanistan + + + Bangladesh + + + Bhutan + + + India + + + Iran (Islamic Republic of) + + + Kazakhstan + + + Kyrgyzstan + + + Maldives + + + Nepal + + + Pakistan + + + Sri Lanka + + + Tajikistan + + + Turkmenistan + + + Uzbekistan + + + + + South-eastern Asia + + + Brunei Darussalam + + + Cambodia + + + Indonesia + + + Lao People's Dem. Republic + + + Malaysia + + + Myanmar + + + Philippines + + + Singapore + + + Thailand + + + Timor-Leste + + + Viet Nam + + + + + Western Asia + + + Armenia + + + Azerbaijan + + + Bahrain + + + Cyprus + + + Georgia + + + Iraq + + + Israel + + + Jordan + + + Kuwait + + + Lebanon + + + Occupied Palestinian Territory + + + Oman + + + Qatar + + + Saudi Arabia + + + Syrian Arab Republic + + + Turkey + + + United Arab Emirates + + + Yemen + + + + + + + EUROPE + + + Eastern Europe + + + Belarus + + + Bulgaria + + + Czech Republic + + + Hungary + + + Poland + + + Republic of Moldova + + + Romania + + + Russian Federation + + + Slovakia + + + Ukraine + + + + + Northern Europe + + + Channel Islands + + + Denmark + + + Estonia + + + Faeroe Islands + + + Finland + + + Iceland + + + Ireland + + + Isle of Man + + + Latvia + + + Lithuania + + + Norway + + + Sweden + + + United Kingdom + + + + + Southern Europe + + + Albania + + + Andorra + + + Bosnia and Herzegovina + + + Croatia + + + Gibraltar + + + Greece + + + Holy See + + + Italy + + + Malta + + + Portugal + + + San Marino + + + Slovenia + + + Spain + + + The Former Yugoslav Republic of Macedonia + + + Yugoslavia + + + + + Western Europe + + + Austria + + + Belgium + + + France + + + Germany + + + Liechtenstein + + + Luxembourg + + + Monaco + + + Netherlands + + + Switzerland + + + + + + + LATIN AMERICA + + + Caribbean + + + Anguilla + + + Antigua and Barbuda + + + Aruba + + + Bahamas + + + Barbados + + + British Virgin Islands + + + Cayman Islands + + + Cuba + + + Dominica + + + Dominican Republic + + + Grenada + + + Guadeloupe + + + Haiti + + + Jamaica + + + Martinique + + + Montserrat + + + Netherlands Antilles + + + Puerto Rico + + + Saint Kitts and Nevis + + + Saint Lucia + + + Saint Vincent and Grenadines. + + + Trinidad and Tobago + + + Turks and Caicos Islands + + + United States Virgin Islands. + + + + + Central America + + + Belize + + + Costa Rica + + + El Salvador + + + Guatemala + + + Honduras + + + Mexico + + + Nicaragua + + + Panama + + + + + South America + + + Argentina + + + Bolivia + + + Brazil + + + Chile + + + Colombia + + + Ecuador + + + Falkland Islands (Malvinas) + + + French Guiana + + + Guyana + + + Paraguay + + + Peru + + + Suriname + + + Uruguay + + + Venezuela + + + + + + + NORTHERN AMERICA + + + Bermuda + + + Canada + + + Greenland + + + Saint Pierre and Miquelon + + + United States of America + + + + + OCEANIA + + + Australia and New Zealand + + + Australia + + + New Zealand + + + Norfolk Island + + + + + Melanesia + + + Fiji + + + New Caledonia + + + Papua New Guinea + + + Solomon Islands + + + Vanuatu + + + + + Micronesia + + + Fed. States of Micronesia + + + Guam + + + Johnston Island + + + Kiribati + + + Marshall Islands + + + Nauru + + + Northern Mariana Islands + + + Palau + + + + + Polynesia + + + American Samoa + + + Cook Islands + + + French Polynesia + + + Niue + + + Pitcairn + + + Samoa + + + Tokelau + + + Tonga + + + Tuvalu + + + Wallis and Futuna Islands + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/bootstrap/descriptor.xml b/config/alfresco/bootstrap/descriptor.xml new file mode 100644 index 0000000000..fceb2cfaa7 --- /dev/null +++ b/config/alfresco/bootstrap/descriptor.xml @@ -0,0 +1,11 @@ + + + + ${version.major} + ${version.minor} + ${version.revision} + ${version.label} + + + diff --git a/config/alfresco/bootstrap/spaces.xml b/config/alfresco/bootstrap/spaces.xml new file mode 100644 index 0000000000..0604768d3b --- /dev/null +++ b/config/alfresco/bootstrap/spaces.xml @@ -0,0 +1,38 @@ + + + + + ${spaces.company_home.name} + space-icon-default + ${spaces.company_home.name} + ${spaces.company_home.description} + + + + ${spaces.dictionary.name} + space-icon-default + ${spaces.dictionary.name} + ${spaces.dictionary.description} + + + + ${spaces.templates.name} + space-icon-default + ${spaces.templates.name} + ${spaces.templates.description} + + + + ${spaces.templates.content.name} + space-icon-default + ${spaces.templates.content.name} + ${spaces.templates.content.description} + + + + + + + diff --git a/config/alfresco/bootstrap/tutorial.xml b/config/alfresco/bootstrap/tutorial.xml new file mode 100644 index 0000000000..b21031de08 --- /dev/null +++ b/config/alfresco/bootstrap/tutorial.xml @@ -0,0 +1,21 @@ + + + + + ${tutorial.space.name} + ${tutorial.space.description} + space-icon-doc + + + + ${tutorial.document.name} + ${tutorial.document.title} + ${tutorial.document.description} + contentUrl=classpath:alfresco/bootstrap/${tutorial.document.name}|mimetype=application/pdf|size=|encoding= + + + + + diff --git a/config/alfresco/cache-context.xml b/config/alfresco/cache-context.xml new file mode 100644 index 0000000000..34c08f4eca --- /dev/null +++ b/config/alfresco/cache-context.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + classpath:ehcache.xml + + + + + + + + + + + + + + + nullPermissionCache + + + + + + + + + + + + + + + + + nullPermissionTransactionalCache + + + 20000 + + + + + + + + + + + + + + + userToAuthorityCache + + + + + + + + + + + + + + + + + userToAuthorityTransactionalCache + + + 20000 + + + + \ No newline at end of file diff --git a/config/alfresco/content-services-context.xml b/config/alfresco/content-services-context.xml new file mode 100644 index 0000000000..35806d09ea --- /dev/null +++ b/config/alfresco/content-services-context.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + application/pdf + + + + + + + + + + + + + + + imconvert ${source} ${options} ${target} + + + imconvert "${source}" ${options} "${target}" + + + + + + + + + + + + + + + + + + + + + + application/msword + text/plain + + + + application/pdf + text/plain + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${dir.contentstore} + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/domain/hibernate-cfg.properties b/config/alfresco/domain/hibernate-cfg.properties new file mode 100644 index 0000000000..d7785afa8c --- /dev/null +++ b/config/alfresco/domain/hibernate-cfg.properties @@ -0,0 +1,19 @@ +# +# Hibernate configuration +# +hibernate.jdbc.use_streams_for_binary=true +hibernate.dialect=org.hibernate.dialect.MySQLDialect +hibernate.show_sql=false +hibernate.hbm2ddl.auto=update +hibernate.cache.use_query_cache=true +hibernate.max_fetch_depth=10 +hibernate.cache.provider_class=org.hibernate.cache.EhCacheProvider +hibernate.cache.use_second_level_cache=true +hibernate.default_batch_fetch_size=1 +hibernate.jdbc.batch_size=32 +hibernate.connection.release_mode=auto + +# +# The cache strategy +# +cache.strategy=read-write diff --git a/config/alfresco/domain/transaction.properties b/config/alfresco/domain/transaction.properties new file mode 100644 index 0000000000..43700be23c --- /dev/null +++ b/config/alfresco/domain/transaction.properties @@ -0,0 +1,9 @@ +# +# Server read-only or read-write modes +# +server.transaction.mode.readOnly=PROPAGATION_REQUIRED, readOnly +# the properties below should change in tandem +# server.transaction.mode=PROPAGATION_REQUIRED, readOnly +# server.transaction.allow-writes=false +server.transaction.mode.default=PROPAGATION_REQUIRED +server.transaction.allow-writes=true diff --git a/config/alfresco/extension/customModel.xml b/config/alfresco/extension/customModel.xml new file mode 100644 index 0000000000..283f781b87 --- /dev/null +++ b/config/alfresco/extension/customModel.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + Custom Model + + 1.0 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/extension/exampleModel.xml b/config/alfresco/extension/exampleModel.xml new file mode 100644 index 0000000000..4d60d17332 --- /dev/null +++ b/config/alfresco/extension/exampleModel.xml @@ -0,0 +1,79 @@ + + + + + + + + + Example custom Model + + 1.0 + + + + + + + + + + + + + + + + + + + + Standard Operating Procedure + cm:content + + + d:datetime + + + d:text + + + + + + cm:content + false + false + + + + + cm:content + false + true + + + + + + + + + + Image Classfication + + + d:int + + + d:int + + + d:int + + + + + + \ No newline at end of file diff --git a/config/alfresco/extension/extension-context.xml b/config/alfresco/extension/extension-context.xml new file mode 100644 index 0000000000..ef063ec596 --- /dev/null +++ b/config/alfresco/extension/extension-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + alfresco/extension/exampleModel.xml + alfresco/extension/customModel.xml + + + + + diff --git a/config/alfresco/file-servers.xml b/config/alfresco/file-servers.xml new file mode 100644 index 0000000000..3942612b34 --- /dev/null +++ b/config/alfresco/file-servers.xml @@ -0,0 +1,70 @@ + + + + + Alfresco CIFS Server + + + 255.255.255.255 + + + + + + + + + + + + + + + + + + + + + workspace://SpacesStore + /app:company_home + + + + + + + + + + + + diff --git a/config/alfresco/hibernate-context-old.xml b/config/alfresco/hibernate-context-old.xml new file mode 100644 index 0000000000..0da179f538 --- /dev/null +++ b/config/alfresco/hibernate-context-old.xml @@ -0,0 +1,71 @@ + + + + + + + + + + classpath:alfresco/domain/hibernate-cfg.properties + + + + + + + true + + + + classpath:alfresco/domain/hibernate-cfg.properties + + + + + + + + + + + + org/alfresco/repo/domain/hibernate/Node.hbm.xml + org/alfresco/repo/domain/hibernate/Store.hbm.xml + org/alfresco/repo/domain/hibernate/VersionCount.hbm.xml + + + + + + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + + + + + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + + + + + + + + SYNCHRONIZATION_ALWAYS + + + + + + + \ No newline at end of file diff --git a/config/alfresco/hibernate-context.xml b/config/alfresco/hibernate-context.xml new file mode 100644 index 0000000000..ad1eaef25c --- /dev/null +++ b/config/alfresco/hibernate-context.xml @@ -0,0 +1,80 @@ + + + + + + + + + + classpath:alfresco/domain/hibernate-cfg.properties + + + + + + + true + + + + classpath:alfresco/domain/hibernate-cfg.properties + + + + + + + + + + + + org/alfresco/repo/domain/hibernate/Node.hbm.xml + org/alfresco/repo/domain/hibernate/Store.hbm.xml + org/alfresco/repo/domain/hibernate/VersionCount.hbm.xml + org/alfresco/repo/security/permissions/impl/hibernate/Permission.hbm.xml + + + + + + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + + + + + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + ${cache.strategy} + + ${cache.strategy} + ${cache.strategy} + + + + + + + + SYNCHRONIZATION_ALWAYS + + + + + + + \ No newline at end of file diff --git a/config/alfresco/index-recovery-context.xml b/config/alfresco/index-recovery-context.xml new file mode 100644 index 0000000000..4d30ed0933 --- /dev/null +++ b/config/alfresco/index-recovery-context.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + workspace://SpacesStore + + + + + \ No newline at end of file diff --git a/config/alfresco/messages/action-config.properties b/config/alfresco/messages/action-config.properties new file mode 100644 index 0000000000..b987bd25a9 --- /dev/null +++ b/config/alfresco/messages/action-config.properties @@ -0,0 +1,70 @@ +# Action conditions + +no-condition.title=All Items +no-condition.description=This condition will match any item added to the space. Use this when you wish to apply an action to everything when it is added to the space. + +compare-property-value.title=Items which contain a specific value in its name +compare-property-value.description=The rule is applied to all items that has a specific value in its name. + +in-category.title=Items with the specified category value +in-category.description=The rule is applied to all items that has the specified category value. + +is-subtype.title=Items of a specified type or its sub-types +is-subtype.description=The rule is applied to all items that are of a specified type or its sub-types + +has-aspect.title=Items that have a specific aspect applied +has-aspect.description=The rule is applied to all items that have the specified aspect applied. + +compare-mime-type.title=Items with the specified mime type +compare-mime-type.description=The rule is applied to all items that have content of the specified mime type. + +# Actions + +add-features.title=Add aspect to item +add-features.description=This will add an aspect to the matched item. + +simple-workflow.title=Add simple workflow to item +simple-workflow.description=This will add a simple workflow to the matched item. This will allow the item to be moved to a different space for its next step in a workflow. You can also give a space for it to be moved to if you want a reject step. + +link-category.title=Link item to category +link-category.description=This will apply a category to the matched item. + +transform.title=Transform and copy content to a specific space +transform.description=This will transform the the matched content and copy the result to a specific space + +transform-image.title=Transform and copy image to a specific space +transform-image.description=This will transform the matched image and copy the result to a specific space + +copy.title=Copy item to a specific space +copy.description=This will copy the matched item to another space. + +move.title=Move item to a specific space +move.description=This will move the matched item to another space. + +mail.title=Send an email to specified users +mail.description=This will send an email to a list of users when the content matches. + +check-in.title=Check in content +check-in.description=This will check in the matched content. + +check-out.title=Check out content +check-out.description=This will check out the matched content. + +set-property-value.title=Set the value of a property +set-property-value.description=This will set the value of a property to a given value. + +import.title=Import an Alfresco content package +import.description=Imports an Alfresco content package into the repository. + +extract-metadata.title=Extract common metadata fields from content +extract-metadata.description=Imports title, author and description metadata fields from common content types. + +specialise-type.title=Specialise the type of an item +specialise-type.description=This will specialise the matched item to a given type. + +export.title=Export a Space +export.description=Exports a Space and optionally it's children to an Alfresco export package. +export.package.description=Alfresco content package for Space ''{0}''. +export.root.package.description=Alfresco content package for complete Repository. +export.package.error=Failed to find temporary file for export + diff --git a/config/alfresco/messages/action-service.properties b/config/alfresco/messages/action-service.properties new file mode 100644 index 0000000000..15cb5fd97b --- /dev/null +++ b/config/alfresco/messages/action-service.properties @@ -0,0 +1,9 @@ +# Action service externalised display strings + +compare_property_value_evaluator.invalid_operation=The operation {0} can not be applied a property of type {1}. +compare_property_value_evaluator.no_content_property=A content proerty must be specified when comparing to a property of type content. +numeric_property_value_comparator.invalid_operation=The operation {0} can not be applied to a numeric property. +text_property_value_comparator.invalid_operation=The operation {0} can not be applied to a text property. +date_property_value_comparator.invalid_operation=The operation {0} can not be applied to a date property. +compare_mime_type_evaluator.not_a_content_type=The specified property is not a content type so mime type can not be compared. +compare_mime_type_evaluator.no_property_definition_found=No defintion can be found for the property specified so mime type can not be compared. diff --git a/config/alfresco/messages/application-model.properties b/config/alfresco/messages/application-model.properties new file mode 100644 index 0000000000..05774d9070 --- /dev/null +++ b/config/alfresco/messages/application-model.properties @@ -0,0 +1,45 @@ +# Display labels for System Model + +app_applicationmodel.description=Alfresco Application Model + +app_applicationmodel.type.app_glossary.title=Data Dictionary +app_applicationmodel.type.app_glossary.description=Data Dictionary + +app_applicationmodel.type.app_configurations.title=Configurations +app_applicationmodel.type.app_configurations.description=Configurations + +app_applicationmodel.aspect.app_uifacets.title=UI Facets +app_applicationmodel.aspect.app_uifacets.description=UI Facets +app_applicationmodel.property.app_icon.title=Icon +app_applicationmodel.property.app_icon.description=Icon + +app_applicationmodel.aspect.app_inlineeditable.title=Inline Editable +app_applicationmodel.aspect.app_inlineeditable.description=Inline Editable +app_applicationmodel.property.app_editInline.title=Edit Inline +app_applicationmodel.property.app_editInline.description=Edit Inline + +app_applicationmodel.aspect.app_inlineeditable.title=Inline Editable +app_applicationmodel.aspect.app_inlineeditable.description=Inline Editable + +app_applicationmodel.aspect.app_workflow.title=Workflow +app_applicationmodel.aspect.app_workflow.description=Workflow + +app_applicationmodel.aspect.app_simpleworkflow.title=Workflow +app_applicationmodel.aspect.app_simpleworkflow.description=Workflow +app_applicationmodel.property.app_approveStep.title=Approve Step +app_applicationmodel.property.app_approveStep.description=Approve Step +app_applicationmodel.property.app_approveFolder.title=Approve Folder +app_applicationmodel.property.app_approveFolder.description=Approve Folder +app_applicationmodel.property.app_approveMove.title=Move or Copy +app_applicationmodel.property.app_approveMove.description=Move or Copy +app_applicationmodel.property.app_rejectStep.title=Reject Step +app_applicationmodel.property.app_rejectStep.description=Reject Step +app_applicationmodel.property.app_rejectFolder.title=Reject Folder +app_applicationmodel.property.app_rejectFolder.description=Reject Folder +app_applicationmodel.property.app_rejectMove.title=Move or Copy +app_applicationmodel.property.app_rejectMove.description=Move or Copy + +app_applicationmodel.aspect.app_configurable.title=Configurable +app_applicationmodel.aspect.app_configurable.description=Configurable +app_applicationmodel.association.app_configurations.title=Configurations +app_applicationmodel.association.app_configurations.description=Configurations diff --git a/config/alfresco/messages/bootstrap-spaces.properties b/config/alfresco/messages/bootstrap-spaces.properties new file mode 100644 index 0000000000..80671a164a --- /dev/null +++ b/config/alfresco/messages/bootstrap-spaces.properties @@ -0,0 +1,13 @@ +# Labels used in bootstrap Space definitions + +spaces.company_home.name=Company Home +spaces.company_home.description=The company root space + +spaces.dictionary.name=Data Dictionary +spaces.dictionary.description=User managed definitions + +spaces.templates.name=Space Templates +spaces.templates.description=Space templates + +spaces.templates.content.name=Content Templates +spaces.templates.content.description=Content templates diff --git a/config/alfresco/messages/bootstrap-templates.properties b/config/alfresco/messages/bootstrap-templates.properties new file mode 100644 index 0000000000..8b8e881574 --- /dev/null +++ b/config/alfresco/messages/bootstrap-templates.properties @@ -0,0 +1,14 @@ +# Labels used in bootstrap Template definitions + +templates.space.project=Software Engineering Project +templates.space.documentation=Documentation +templates.space.drafts=Drafts +templates.space.pending_approval=Pending Approval +templates.space.published=Published +templates.space.samples=Samples +templates.document.system_overview.title=System Overview +templates.document.system_overview.name=system-overview.html +templates.space.discussions=Discussions +templates.space.ui_design=UI Design +templates.space.presentations=Presentations +templates.space.quality_assurance=Quality Assurance \ No newline at end of file diff --git a/config/alfresco/messages/bootstrap-tutorial.properties b/config/alfresco/messages/bootstrap-tutorial.properties new file mode 100644 index 0000000000..56011fbb51 --- /dev/null +++ b/config/alfresco/messages/bootstrap-tutorial.properties @@ -0,0 +1,9 @@ +# Labels used in bootstrap Tutorial definitions + +tutorial.space.name=Alfresco Tutorial +tutorial.space.description=Step by step guide to the Alfresco application + +tutorial.document.name=Alfresco-Tutorial.pdf +tutorial.document.title=Alfresco Tutorial +tutorial.document.description=Getting started guide + diff --git a/config/alfresco/messages/coci-service.properties b/config/alfresco/messages/coci-service.properties new file mode 100644 index 0000000000..034bcbfd6f --- /dev/null +++ b/config/alfresco/messages/coci-service.properties @@ -0,0 +1,8 @@ +# coci service externalised display strings + +coci_service.working_copy_label=(Working Copy) +coci_service.err_bad_copy=The original node can not be found. Perhaps the copy has been corrupted or the original has been deleted or moved. +coci_service.err_not_owner=This user is not the owner of the working copy and can not check it in. +coci_service.err_workingcopy_checkout=A working copy can not be checked out. +coci_service.err_not_authenticated=Can not find the currently authenticated user details required by the CheckOutCheckIn service. +coci_service.err_workingcopy_has_no_mimetype=Working copy node ({0}) has no mimetype diff --git a/config/alfresco/messages/content-model.properties b/config/alfresco/messages/content-model.properties new file mode 100644 index 0000000000..ff55684c43 --- /dev/null +++ b/config/alfresco/messages/content-model.properties @@ -0,0 +1,201 @@ +# Display labels for Content Domain Model + +cm_contentmodel.description=Alfresco Content Domain Model + +cm_contentmodel.type.cm_object.title=Object +cm_contentmodel.type.cm_object.description=Base Content Domain Object +cm_contentmodel.property.cm_name.title=Name +cm_contentmodel.property.cm_name.description=Name + +cm_contentmodel.type.cm_folder.title=Folder +cm_contentmodel.type.cm_folder.description=Folder +cm_contentmodel.property.cm_orderedchildren.title=Ordered Children +cm_contentmodel.property.cm_orderedchildren.description=Indicates whether the children of the folder are ordered +cm_contentmodel.association.cm_contains.title=Contains +cm_contentmodel.association.cm_contains.description=Contains + +cm_contentmodel.type.cm_content.title=Content +cm_contentmodel.type.cm_content.description=Base Content Object +cm_contentmodel.property.cm_content.title=Content +cm_contentmodel.property.cm_content.description=Content + +cm_contentmodel.type.cm_linkfile.title=File Link +cm_contentmodel.type.cm_linkfile.description=Link to another File +cm_contentmodel.property.cm_path.title=Link File Path +cm_contentmodel.property.cm_path.description=Path to the linked File + +cm_contentmodel.type.cm_savedquery.title=Saved Query +cm_contentmodel.type.cm_savedquery.description=Saved Query + +cm_contentmodel.type.cm_systemfolder.title=System Folder +cm_contentmodel.type.cm_systemfolder.description=Folder for containing system-level items + +cm_contentmodel.type.cm_person.title=Person +cm_contentmodel.type.cm_person.description=Person + +cm_contentmodel.property.cm_userName.title=User Name +cm_contentmodel.property.cm_userName.description=The Person's user name +cm_contentmodel.property.cm_homeFolder.title=Home Folder +cm_contentmodel.property.cm_homeFolder.description=The Person's home folder +cm_contentmodel.property.cm_firstName.title=First Name +cm_contentmodel.property.cm_firstName.description=The Person's first name +cm_contentmodel.property.cm_lastName.title=Last Name +cm_contentmodel.property.cm_lastName.description=The Person's last name +cm_contentmodel.property.cm_middleName.title=Middle Name +cm_contentmodel.property.cm_middleName.description=The Person's middle name +cm_contentmodel.property.cm_email.title=E-mail Address +cm_contentmodel.property.cm_email.description=The Person's e-mail address +cm_contentmodel.property.cm_organizationId.title=Organization +cm_contentmodel.property.cm_organizationId.description=The Person's organization + +cm_contentmodel.type.cm_category_root.title=Category Root +cm_contentmodel.type.cm_category_root.description=Root Category +cm_contentmodel.association.cm_categories.title=Categories +cm_contentmodel.association.cm_categories.description=Categories within Category Root + +cm_contentmodel.type.cm_category.title=Category +cm_contentmodel.type.cm_category.description=Category +cm_contentmodel.association.cm_subcategories.title=Categories +cm_contentmodel.association.cm_subcategories.description=Sub-categories within Category + +cm_contentmodel.aspect.cm_titled.title=Titled +cm_contentmodel.aspect.cm_titled.description=Titled +cm_contentmodel.property.cm_created.title=Created Date +cm_contentmodel.property.cm_created.description=Created Date +cm_contentmodel.property.cm_creator.title=Creator +cm_contentmodel.property.cm_creator.description=Who created this item +cm_contentmodel.property.cm_modified.title=Modified Date +cm_contentmodel.property.cm_modified.description=When this item as last modified +cm_contentmodel.property.cm_modifier.title=Modifier +cm_contentmodel.property.cm_modifier.description=Who last modified this item +cm_contentmodel.property.cm_accessed.title=Last Accessed Date +cm_contentmodel.property.cm_accessed.description=When this item was last accessed + +cm_contentmodel.aspect.cm_localizable.title=Localizable +cm_contentmodel.aspect.cm_localizable.description=Localizable +cm_contentmodel.property.cm_locale.title=Locale +cm_contentmodel.property.cm_locale.description=Locale + +cm_contentmodel.aspect.cm_translatable.title=Translatable +cm_contentmodel.aspect.cm_translatable.description=Translatable +cm_contentmodel.association.cm_translations.title=Translations +cm_contentmodel.association.cm_translations.description=Translations + +cm_contentmodel.aspect.cm_transformable.title=Transformable +cm_contentmodel.aspect.cm_transformable.description=Transformable +cm_contentmodel.association.cm_formats.title=Formats +cm_contentmodel.association.cm_formats.description=Transformed Items + +cm_contentmodel.aspect.cm_templatable.title=Templatable +cm_contentmodel.aspect.cm_templatable.description=Templatable +cm_contentmodel.aspect.cm_template.title=Template +cm_contentmodel.aspect.cm_template.description=Template + +cm_contentmodel.aspect.cm_complianceable.title=Complianceable +cm_contentmodel.aspect.cm_complianceable.description=Complianceable +cm_contentmodel.property.cm_removeAfter.title=Remove After +cm_contentmodel.property.cm_removeAfter.description=Remove After + +cm_contentmodel.aspect.cm_ownable.title=Ownable +cm_contentmodel.aspect.cm_ownable.description=Ownable +cm_contentmodel.property.cm_owner.title=Owner +cm_contentmodel.property.cm_owner.description=Owner + +cm_contentmodel.aspect.cm_dublincore.title=Dublin Core +cm_contentmodel.aspect.cm_dublincore.description=Dublin Core +cm_contentmodel.property.cm_publisher.title=Publisher +cm_contentmodel.property.cm_publisher.description=Publisher +cm_contentmodel.property.cm_contributor.title=Contributor +cm_contentmodel.property.cm_contributor.description=Contributor +cm_contentmodel.property.cm_type.title=Type +cm_contentmodel.property.cm_type.description=Type +cm_contentmodel.property.cm_identifier.title=Identifier +cm_contentmodel.property.cm_identifier.description=Identifier +cm_contentmodel.property.cm_dcsource.title=Source +cm_contentmodel.property.cm_dcsource.description=Source +cm_contentmodel.property.cm_coverage.title=Coverage +cm_contentmodel.property.cm_coverage.description=Coverage +cm_contentmodel.property.cm_rights.title=Rights +cm_contentmodel.property.cm_rights.description=Rights +cm_contentmodel.property.cm_subject.title=Subject +cm_contentmodel.property.cm_subject.description=Subject + +cm_contentmodel.aspect.cm_basable.title=Basable +cm_contentmodel.aspect.cm_basable.description=Basable +cm_contentmodel.association.cm_basis.title=Basis +cm_contentmodel.association.cm_basis.description=Basis + +cm_contentmodel.aspect.cm_partable.title=Partable +cm_contentmodel.aspect.cm_partable.description=Partable +cm_contentmodel.association.cm_parts.title=Parts +cm_contentmodel.association.cm_parts.description=Parts + +cm_contentmodel.aspect.cm_referencing.title=Referencing +cm_contentmodel.aspect.cm_referencing.description=Referencing +cm_contentmodel.association.cm_references.title=References +cm_contentmodel.association.cm_references.description=References + +cm_contentmodel.aspect.cm_replacable.title=Replacable +cm_contentmodel.aspect.cm_replacable.description=Replacable +cm_contentmodel.association.cm_replaces.title=Replaces +cm_contentmodel.association.cm_replaces.description=Replaces + +cm_contentmodel.aspect.cm_effectivity.title=Effectivity +cm_contentmodel.aspect.cm_effectivity.description=Effectivity +cm_contentmodel.property.cm_from.title=Effective From +cm_contentmodel.property.cm_from.description=Effective From +cm_contentmodel.property.cm_to.title=Effective To +cm_contentmodel.property.cm_to.description=Effective To + +cm_contentmodel.aspect.cm_summarizable.title=Summarizable +cm_contentmodel.aspect.cm_summarizable.description=Summarizable +cm_contentmodel.property.cm_summary.title=Summary +cm_contentmodel.property.cm_summary.description=Summary + +cm_contentmodel.aspect.cm_countable.title=Countable +cm_contentmodel.aspect.cm_countable.description=Countable +cm_contentmodel.property.cm_hits.title=Hits +cm_contentmodel.property.cm_hits.description=Hits + +cm_contentmodel.aspect.cm_copiedFrom.title=Copied From +cm_contentmodel.aspect.cm_copiedFrom.description=Copied From +cm_contentmodel.property.cm_source.title=Source +cm_contentmodel.property.cm_source.description=Source + +cm_contentmodel.aspect.cm_workingcopy.title=Working Copy +cm_contentmodel.aspect.cm_workingcopy.description=Working Copy +cm_contentmodel.property.cm_workingCopyOwner.title=Working Copy Owner +cm_contentmodel.property.cm_workingCopyOwner.description=Working Copy Owner + +cm_contentmodel.aspect.cm_versionable.title=Versionable +cm_contentmodel.aspect.cm_versionable.description=Versionable +cm_contentmodel.property.cm_versionLabel.title=Version Label +cm_contentmodel.property.cm_versionLabel.description=Version Label + +cm_contentmodel.aspect.cm_lockable.title=Lockable +cm_contentmodel.aspect.cm_lockable.description=Lockable +cm_contentmodel.property.cm_lockOwner.title=Lock Owner +cm_contentmodel.property.cm_lockOwner.description=Lock Owner +cm_contentmodel.property.cm_lockType.title=Lock Type +cm_contentmodel.property.cm_lockType.description=Lock Type +cm_contentmodel.property.cm_expiryDate.title=Expiry Date +cm_contentmodel.property.cm_expiryDate.description=Expiry Date +cm_contentmodel.property.cm_lockIsDeep.title=Deep Lock +cm_contentmodel.property.cm_lockIsDeep.description=Deep Lock + +cm_contentmodel.aspect.cm_subscribable.title=Subscribable +cm_contentmodel.aspect.cm_subscribable.description=Subscribable +cm_contentmodel.association.cm_subscribedBy.title=Subscribed By +cm_contentmodel.association.cm_subscribedBy.description=Subscribed By + +cm_contentmodel.aspect.cm_classifiable.title=Classifiable +cm_contentmodel.aspect.cm_classifiable.description=Classifiable + +cm_contentmodel.aspect.cm_generalclassifiable.title=Classifiable +cm_contentmodel.aspect.cm_generalclassifiable.description=Classifiable +cm_contentmodel.property.cm_categories.title=Categories +cm_contentmodel.property.cm_categories.description=Categories + +cm_contentmodel.aspect.cm_attachable.title=Attachable +cm_contentmodel.aspect.cm_attachable.description=Allows other repository objects to be attached + diff --git a/config/alfresco/messages/content-service.properties b/config/alfresco/messages/content-service.properties new file mode 100644 index 0000000000..faf2337916 --- /dev/null +++ b/config/alfresco/messages/content-service.properties @@ -0,0 +1,3 @@ +# Content-related messages + +content.content_missing=The node''s content is missing: \n node: {0} \n reader: {1} \n Please contact your system administrator. diff --git a/config/alfresco/messages/dictionary-model.properties b/config/alfresco/messages/dictionary-model.properties new file mode 100644 index 0000000000..79ffd2954e --- /dev/null +++ b/config/alfresco/messages/dictionary-model.properties @@ -0,0 +1,34 @@ +# Display labels for Dictionary Model + +d_dictionary.description=Alfresco Dictionary Model + +d_dictionary.datatype.d_any.title=Any +d_dictionary.datatype.d_any.description=Any +d_dictionary.datatype.d_text.title=Text +d_dictionary.datatype.d_text.description=Text +d_dictionary.datatype.d_content.title=Content +d_dictionary.datatype.d_content.description=Content +d_dictionary.datatype.d_int.title=Integer +d_dictionary.datatype.d_int.description=Integer +d_dictionary.datatype.d_long.title=Long +d_dictionary.datatype.d_long.description=Long +d_dictionary.datatype.d_float.title=Float +d_dictionary.datatype.d_float.description=Float +d_dictionary.datatype.d_double.title=Double +d_dictionary.datatype.d_double.description=Double +d_dictionary.datatype.d_date.title=Date +d_dictionary.datatype.d_date.description=Date +d_dictionary.datatype.d_datetime.title=Date and Time +d_dictionary.datatype.d_datetime.description=Date and Time +d_dictionary.datatype.d_boolean.title=Boolean +d_dictionary.datatype.d_boolean.description=Boolean +d_dictionary.datatype.d_qname.title=Qualified Name +d_dictionary.datatype.d_qname.description=Qualified Name +d_dictionary.datatype.d_guid.title=Unique Identifier +d_dictionary.datatype.d_guid.description=Unique Identifier +d_dictionary.datatype.d_category.title=Category +d_dictionary.datatype.d_category.description=Category +d_dictionary.datatype.d_noderef.title=Reference +d_dictionary.datatype.d_noderef.description=Reference +d_dictionary.datatype.d_path.title=Path +d_dictionary.datatype.d_path.description=Path diff --git a/config/alfresco/messages/forum-model.properties b/config/alfresco/messages/forum-model.properties new file mode 100644 index 0000000000..36109047af --- /dev/null +++ b/config/alfresco/messages/forum-model.properties @@ -0,0 +1,19 @@ +# Display labels for System Model + +fm_forummodel.description=Forum Model + +fm_forummodel.type.fm_forums.title=Forums + +fm_forummodel.type.fm_forum.title=Forum +fm_forummodel.property.fm_status.title=Forum Status +fm_forummodel.property.fm_status.description=Status of forum i.e. locked, read-only + +fm_forummodel.type.fm_topic.title=Topic +fm_forummodel.property.fm_type.title=Topic Type +fm_forummodel.property.fm_type.description=Type of topic i.e. sticky, announcement etc. + +fm_forummodel.type.fm_post.title=Forum Article + +fm_forummodel.aspect.fm_discussable.title=Discussable +fm_forummodel.property.fm_forum.title=Forum +fm_forummodel.property.fm_forum.description=The forum holding the discussion on the object the aspect is applied to diff --git a/config/alfresco/messages/permissions-service.properties b/config/alfresco/messages/permissions-service.properties new file mode 100644 index 0000000000..368e3f7c82 --- /dev/null +++ b/config/alfresco/messages/permissions-service.properties @@ -0,0 +1 @@ +permissions.err_access_denied=Access Denied. You do not have the appropriate permissions to perform this operation. diff --git a/config/alfresco/messages/rule-config.properties b/config/alfresco/messages/rule-config.properties new file mode 100644 index 0000000000..6907e15b5c --- /dev/null +++ b/config/alfresco/messages/rule-config.properties @@ -0,0 +1,4 @@ +# Rule types + +inbound.display-label=Inbound +outbound.display-label=Outbound diff --git a/config/alfresco/messages/system-model.properties b/config/alfresco/messages/system-model.properties new file mode 100644 index 0000000000..3f8c4e0bf0 --- /dev/null +++ b/config/alfresco/messages/system-model.properties @@ -0,0 +1,31 @@ +# Display labels for System Model + +sys_systemmodel.description=Alfresco System Model + +sys_systemmodel.type.sys_base.title=Base +sys_systemmodel.type.sys_base.description=Base + +sys_systemmodel.type.sys_container.title=Container +sys_systemmodel.type.sys_container.description=Container +sys_systemmodel.association.sys_children.title=Children +sys_systemmodel.association.sys_children.description=Children + +sys_systemmodel.type.sys_store_root.title=Store Root +sys_systemmodel.type.sys_store_root.description=Store Root + +sys_systemmodel.type.sys_reference.title=Reference +sys_systemmodel.type.sys_reference.description=Reference +sys_systemmodel.property.sys_reference.title=Reference +sys_systemmodel.property.sys_reference.description=Reference + +sys_systemmodel.aspect.aspect_root.title=Root +sys_systemmodel.aspect.aspect_root.description=Root + +sys_systemmodel.aspect.sys_referenceable.title=Referenceable +sys_systemmodel.aspect.sys_referenceable.description=Referenceable +sys_systemmodel.property.sys_store-protocol.title=Store Protocol +sys_systemmodel.property.sys_store-protocol.description=Store Protocol +sys_systemmodel.property.sys_store-identifier.title=Store Identifier +sys_systemmodel.property.sys_store-identifier.description=Store Identifier +sys_systemmodel.property.sys_node-uuid.title=Node Identifier +sys_systemmodel.property.sys_node-uuid.description=Node Identifier diff --git a/config/alfresco/messages/template-service.properties b/config/alfresco/messages/template-service.properties new file mode 100644 index 0000000000..673bc3fa6d --- /dev/null +++ b/config/alfresco/messages/template-service.properties @@ -0,0 +1,5 @@ +# Template Service externalised display strings + +error_no_template=Unable to find the template ''{0}''. Please contact your system adminstrator. +error_template_fail=Error during processing of the template ''{0}''. Please contact your system adminstrator. +error_template_io=IO Error during processing of the template ''{0}''. Please contact your system adminstrator. diff --git a/config/alfresco/messages/version-service.properties b/config/alfresco/messages/version-service.properties new file mode 100644 index 0000000000..6d7a741cfb --- /dev/null +++ b/config/alfresco/messages/version-service.properties @@ -0,0 +1,8 @@ +# Rule service externalised display strings + +version_service.err_restore_exists=The node {0} cannot be restored since it already exists. +version_service.err_not_found=The current version label of the node does not exist in the version history. +version_service.err_unsupported=The current implementation of the version service does not support the creation of branches. +version_service.err_one_preceeding=The current implementation of the version service only supports one preceeding version. +version_service.err_restore_no_version=The node {0} cannot be restore since there is no version information available for this node. +version_service.err_revert_mismatch=The version provided to revert to does not come from the nodes version history. diff --git a/config/alfresco/mimetype-map.xml b/config/alfresco/mimetype-map.xml new file mode 100644 index 0000000000..25774a31ec --- /dev/null +++ b/config/alfresco/mimetype-map.xml @@ -0,0 +1,341 @@ + + + + + + txt + csv + java + sql + properties + ftl + ini + bat + sh + log + + + + html + htm + shtml + body + + + xhtml + + + + ai + eps + ps + + + aiff + aif + aifc + + + acp + + + au + snd + + + avi + qvi + + + asf + asx + + + wmv + + + wma + + + avx + + + bcpio + + + bin + exe + + + cdf + nc + + + cer + + + cgm + + + class + + + cpio + + + csh + + + css + + + doc + + + xml + dtd + xslt + xsl + + + dvi + + + etx + + + gif + + + gml + + + gtar + + + gzip + + + hdf + + + hqx + + + ief + + + bmp + + + jpg + jpeg + jpe + + + js + + + latex + + + man + + + me + + + ms + + + mif + + + mpg + mpeg + mpe + + + mp3 + mp2 + + + mp4 + + + mpeg2 + + + mov + qt + + + movie + mpv2 + + + oda + + + pbm + + + pdf + + + pgm + + + png + + + pnm + rpnm + + + ppm + + + ppt + + + ras + + + rgb + + + tr + t + roff + + + rtf + + + rtx + + + sgml + sgm + + + sh + + + shar + + + src + + + sv4cpio + + + sv4crc + + + swf + + + tar + + + tcl + + + tex + + + texinfo + texi + + + tiff + tif + + + tsv + + + ustar + + + wav + + + wrl + + + xbm + + + xls + + + xpm + + + xwd + + + z + + + zip + + + odt + + + ott + + + oth + + + odm + + + odg + + + otg + + + odp + + + otp + + + ods + + + ots + + + odc + + + odf + + + odb + + + odi + + + sxw + + + dwg + + + dwt + + + + + diff --git a/config/alfresco/model-specific-services-context.xml b/config/alfresco/model-specific-services-context.xml new file mode 100644 index 0000000000..2ae11ec15d --- /dev/null +++ b/config/alfresco/model-specific-services-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/model/applicationModel.xml b/config/alfresco/model/applicationModel.xml new file mode 100644 index 0000000000..b6427bb78b --- /dev/null +++ b/config/alfresco/model/applicationModel.xml @@ -0,0 +1,119 @@ + + + Alfresco Application Model + Alfresco + 2005-09-29 + 1.0 + + + + + + + + + + + + + + + + cm:folder + + + + + Configurations + cm:systemfolder + + + + + + + + UI Facets + cm:titled + + + d:text + + + + + + Inline Editable + + + Edit Inline + d:boolean + + + + + + + Workflow + + + + app:workflow + + + d:text + true + + + d:noderef + true + + + d:boolean + + + d:text + true + + + d:noderef + true + + + d:boolean + + + + + + Configurable + + + + false + false + + + app:configurations + false + false + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/model/contentModel.xml b/config/alfresco/model/contentModel.xml new file mode 100644 index 0000000000..f3769b75a7 --- /dev/null +++ b/config/alfresco/model/contentModel.xml @@ -0,0 +1,624 @@ + + + Alfresco Content Domain Model + Alfresco + 2005-09-29 + 1.0 + + + + + + + + + + + + + + + Object + sys:base + + + Name + d:text + true + + + + cm:auditable + + + + + Folder + cm:cmobject + + + d:boolean + + + + + + false + false + + + sys:base + false + true + + false + + + + + + Content + cm:cmobject + + + d:content + false + + + true + false + true + + + + + + + Dictionary Model + cm:content + + + Model name + d:qname + true + + + Description + d:text + true + + + Author + d:text + true + + + Published Date + d:date + true + + + Version + d:text + true + + + Model Active + d:boolean + false + + + + + + Link File + cm:cmobject + + + d:path + + + + + + Saved Query + cm:content + + + + System Folder + cm:folder + + + + Person + sys:base + + + d:text + true + + + d:noderef + true + + + d:text + true + + + d:text + true + + + d:text + + + d:text + + + d:text + + + + + + + + Category Root + cm:cmobject + + + + cm:category + + + + + sys:aspect_root + + + + + Category + cm:cmobject + + + + cm:category + + + + + + + + + + + + Titled + + + Title + d:text + + + Description + d:text + + + + + + Auditable + + + Created + d:datetime + + + Author + d:text + + + Modified + d:datetime + + + Modifier + d:text + + + Accessed + d:datetime + + + + + + Localizable + + + Locale + + d:category + + + + + + Translatable + cm:localizable + + + + Translations + + cm:translationOf + false + false + + + cm:content + cm:hasTranslation + false + true + + + + + + + Transformable + + + Formats + + cm:formatOf + false + false + + + cm:content + cm:hasFormat + false + true + + + + + + + Templatable + + + Template + d:noderef + false + + + + + + Complianceable + cm:auditable + + + + Remove After + d:datetime + + + + + + Ownable + + + Owner + d:text + + + + + + Dublin Core + cm:titled + + + + Publisher + d:text + + + Contributor + d:text + + + Type + d:text + + + Identifier + d:text + + + Source + d:text + + + Coverage + d:text + + + Rights + d:text + + + Subject + d:text + + + + + + + + Basable + + + + cm:basedOn + false + true + + + cm:content + cm:hasBasis + false + true + + + + + + + Partable + + + + cm:partOf + false + true + + + cm:content + cm:hasPart + false + true + + + + + + + Referencing + + + + cm:referencedBy + false + true + + + cm:content + cm:references + false + true + + + + + + + Replacable + + + + cm:replacedBy + false + true + + + cm:content + cm:replaces + false + true + + + + + + + Effectivity + + + Effective From + d:datetime + + + Effective To + d:datetime + + + + + + Summarizable + + + Summary + d:text + + + + + + Countable + + + d:int + + + + + + Copied From + + + d:noderef + true + true + false + + true + false + true + + + + + + + Working Copy + + + d:text + true + true + + + + + + Versionable + + + Version Label + d:text + true + + + Auto Version + d:boolean + true + + + + + + Lockable + + + d:text + true + + + d:text + true + + + d:date + true + false + + + d:boolean + true + + + + + + + + + false + true + + + cm:person + false + true + + + + + + + Classifiable + + + + General Classifiable + cm:classifiable + + + Categories + d:category + false + true + + true + true + true + + + + + + + Attachable + + + + false + true + + + cm:cmobject + false + true + + + + + + + + diff --git a/config/alfresco/model/dataTypeAnalyzers.properties b/config/alfresco/model/dataTypeAnalyzers.properties new file mode 100644 index 0000000000..75b6672692 --- /dev/null +++ b/config/alfresco/model/dataTypeAnalyzers.properties @@ -0,0 +1,17 @@ +# Data Type Index Analyzers + +d_dictionary.datatype.d_any.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_text.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_content.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_int.analyzer=org.alfresco.repo.search.impl.lucene.analysis.IntegerAnalyser +d_dictionary.datatype.d_long.analyzer=org.alfresco.repo.search.impl.lucene.analysis.LongAnalyser +d_dictionary.datatype.d_float.analyzer=org.alfresco.repo.search.impl.lucene.analysis.FloatAnalyser +d_dictionary.datatype.d_double.analyzer=org.alfresco.repo.search.impl.lucene.analysis.DoubleAnalyser +d_dictionary.datatype.d_date.analyzer=org.alfresco.repo.search.impl.lucene.analysis.DateAnalyser +d_dictionary.datatype.d_datetime.analyzer=org.alfresco.repo.search.impl.lucene.analysis.DateAnalyser +d_dictionary.datatype.d_boolean.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_qname.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_guid.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_category.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_noderef.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer +d_dictionary.datatype.d_path.analyzer=org.apache.lucene.analysis.standard.StandardAnalyzer diff --git a/config/alfresco/model/dictionaryModel.xml b/config/alfresco/model/dictionaryModel.xml new file mode 100644 index 0000000000..8b99b02613 --- /dev/null +++ b/config/alfresco/model/dictionaryModel.xml @@ -0,0 +1,96 @@ + + + Alfresco Dictionary Model + Alfresco + 2005-09-29 + 1.0 + + + + + + + + + + + + + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + java.lang.Object + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + java.lang.String + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + org.alfresco.service.cmr.repository.ContentData + + + + org.alfresco.repo.search.impl.lucene.analysis.IntegerAnalyser + java.lang.Integer + + + + org.alfresco.repo.search.impl.lucene.analysis.LongAnalyser + java.lang.Long + + + + org.alfresco.repo.search.impl.lucene.analysis.FloatAnalyser + java.lang.Float + + + + org.alfresco.repo.search.impl.lucene.analysis.DoubleAnalyser + java.lang.Double + + + + org.alfresco.repo.search.impl.lucene.analysis.DateAnalyser + java.util.Date + + + + org.alfresco.repo.search.impl.lucene.analysis.DateAnalyser + java.util.Date + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + java.lang.Boolean + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + org.alfresco.service.namespace.QName + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + org.alfresco.service.cmr.repository.NodeRef + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + org.alfresco.service.cmr.repository.Path + + + + org.apache.lucene.analysis.standard.StandardAnalyzer + org.alfresco.service.cmr.repository.NodeRef + + + + + + + + diff --git a/config/alfresco/model/forumModel.xml b/config/alfresco/model/forumModel.xml new file mode 100644 index 0000000000..64ac81ff2d --- /dev/null +++ b/config/alfresco/model/forumModel.xml @@ -0,0 +1,60 @@ + + + + + Forum Model + + 1.0 + + + + + + + + + + + + + + + + cm:folder + + + + cm:folder + + + d:category + + + + + + cm:folder + + + d:category + + + + + + cm:content + + + + + + + + d:noderef + true + + + + + + \ No newline at end of file diff --git a/config/alfresco/model/modelSchema.xsd b/config/alfresco/model/modelSchema.xsd new file mode 100644 index 0000000000..7d884c4add --- /dev/null +++ b/config/alfresco/model/modelSchema.xsd @@ -0,0 +1,212 @@ + + + + + + Alfresco Data Dictionary Schema - DRAFT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/model/permissionDefinitions.xml b/config/alfresco/model/permissionDefinitions.xml new file mode 100644 index 0000000000..d260f97a22 --- /dev/null +++ b/config/alfresco/model/permissionDefinitions.xml @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/model/permissionSchema.dtd b/config/alfresco/model/permissionSchema.dtd new file mode 100644 index 0000000000..00da147657 --- /dev/null +++ b/config/alfresco/model/permissionSchema.dtd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml new file mode 100644 index 0000000000..ceb2f1ae18 --- /dev/null +++ b/config/alfresco/model/systemModel.xml @@ -0,0 +1,114 @@ + + + Alfresco Repository System Definitions + Alfresco + 2005-09-29 + 1.0 + + + + + + + + + + + + + Base + + sys:referenceable + + + + + Descriptor + sys:base + + + d:text + true + + + d:text + true + + + d:text + true + + + d:text + + + + + + Container + sys:base + + + + false + true + + + sys:base + false + true + + + + + + + Store Root + sys:container + + sys:aspect_root + + + + + Reference + sys:base + + + d:noderef + true + + + + + + + + + + + Root + + + + + Referenceable + + + d:text + true + + + d:text + true + + + d:text + true + + + + + + + \ No newline at end of file diff --git a/config/alfresco/network-protocol-context.xml b/config/alfresco/network-protocol-context.xml new file mode 100644 index 0000000000..ad78c1c9ba --- /dev/null +++ b/config/alfresco/network-protocol-context.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + alfresco/file-servers.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/node-services-context.xml b/config/alfresco/node-services-context.xml new file mode 100644 index 0000000000..cf2676e6b7 --- /dev/null +++ b/config/alfresco/node-services-context.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.node.db.NodeDaoService + + + + + + + dbNodeDaoServiceTxnRegistration + + + + + + + org.alfresco.service.cmr.repository.NodeService + + + + + + + + + + + + + + + + org.alfresco.service.cmr.repository.NodeService + + + + + + + + + + + + + + + + + + + + + + + + + dbNodeService + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + false + + + 5 + + + + diff --git a/config/alfresco/ownable-services-context-old.xml b/config/alfresco/ownable-services-context-old.xml new file mode 100644 index 0000000000..63c922ad04 --- /dev/null +++ b/config/alfresco/ownable-services-context-old.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/config/alfresco/ownable-services-context.xml b/config/alfresco/ownable-services-context.xml new file mode 100644 index 0000000000..89106a7ef9 --- /dev/null +++ b/config/alfresco/ownable-services-context.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml new file mode 100644 index 0000000000..9f3bdd6a54 --- /dev/null +++ b/config/alfresco/public-services-context.xml @@ -0,0 +1,864 @@ + + + + + + + + + + http://www.alfresco.org + + + + + + + + + org.alfresco.service.ServiceRegistry + + + + + + + + + + + + + + org.alfresco.service.ServiceRegistry + + + Repository service registry + + + + + + + + + + + + org.alfresco.service.descriptor.DescriptorService + + + + + + + + + + + + + org.alfresco.service.descriptor.DescriptorService + + + Descriptor service + + + + + + + + + org.alfresco.service.namespace.NamespaceService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.namespace.NamespaceService + + + Namespace service + + + + + + + + + + + + + + org.alfresco.service.cmr.dictionary.DictionaryService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.dictionary.DictionaryService + + + Dictionary Service + + + + + + + + + org.alfresco.service.cmr.repository.NodeService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.readOnly} + ${server.transaction.mode.readOnly} + ${server.transaction.mode.readOnly} + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.repository.NodeService + + + Node Service + + + + + + + + + + + org.alfresco.service.cmr.repository.ContentService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.repository.ContentService + + + Content Service + + + + + + + + + org.alfresco.service.cmr.repository.MimetypeService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.repository.MimetypeService + + + Mime Type Service + + + + + + + + + org.alfresco.service.cmr.search.SearchService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.search.SearchService + + + Search Service + + + + + + + + + org.alfresco.service.cmr.search.CategoryService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.search.CategoryService + + + Category Service + + + + + + + + + org.alfresco.service.cmr.repository.CopyService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.repository.CopyService + + + Copy Service + + + + + + + + + org.alfresco.service.cmr.lock.LockService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.lock.LockService + + + Lock Service + + + + + + + + + org.alfresco.service.cmr.version.VersionService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.version.VersionService + + + Version Service + + + + + + + + + org.alfresco.service.cmr.coci.CheckOutCheckInService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.coci.CheckOutCheckInService + + + Version Service + + + + + + + + + org.alfresco.service.cmr.rule.RuleService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.rule.RuleService + + + Rule Service + + + + + + + + + org.alfresco.service.cmr.view.ImporterService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.view.ImporterService + + + Importer Service + + + + + + + + + org.alfresco.service.cmr.view.ExporterService + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.view.ExporterService + + + Exporter Service + + + + + + + + + org.alfresco.service.cmr.action.ActionService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.action.ActionService + + + Action Service + + + + + + + + + org.alfresco.service.cmr.security.PermissionService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.security.PermissionService + + + Permission Service + + + + + + + + + org.alfresco.service.cmr.security.AuthorityService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.security.AuthorityService + + + Authority Service + + + + + + + + + org.alfresco.service.cmr.security.OwnableService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.security.OwnableService + + + OwnableService Service + + + + + + + + + + org.alfresco.service.cmr.security.AuthenticationService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.security.AuthenticationService + + + AuthenticationService Service + + + + + + + org.alfresco.service.cmr.repository.TemplateService + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.repository.TemplateService + + + TemplateService Service + + + + + + + org.alfresco.service.cmr.model.FileFolderService + + + + + + + + + + + + + + + + + ${server.transaction.mode.readOnly} + ${server.transaction.mode.readOnly} + ${server.transaction.mode.readOnly} + ${server.transaction.mode.readOnly} + ${server.transaction.mode.default} + + + + + diff --git a/config/alfresco/public-services-security-context-old.xml b/config/alfresco/public-services-security-context-old.xml new file mode 100644 index 0000000000..2dc4b73e38 --- /dev/null +++ b/config/alfresco/public-services-security-context-old.xml @@ -0,0 +1,64 @@ + + + + + + + + + + org.alfresco.repo.security.permissions.PermissionServiceSPI + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml new file mode 100644 index 0000000000..e994c2e804 --- /dev/null +++ b/config/alfresco/public-services-security-context.xml @@ -0,0 +1,611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.security.permissions.PermissionServiceSPI + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + alfresco/model/permissionDefinitions.xml + + + + + + + + + + + + + + + + + + + + ROLE_ + + + + + + + + + + GROUP_ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.repository.NodeService.getStores=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.createStore=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.repository.NodeService.exists=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.getNodeStatus=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.getRootNode=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.createNode=ACL_NODE.0.sys:base.CreateChildren + org.alfresco.service.cmr.repository.NodeService.moveNode=ACL_NODE.0.sys:base.WriteProperties,ACL_PARENT.0.sys:base.DeleteChildren,ACL_NODE.1.sys:base.CreateChildren + org.alfresco.service.cmr.repository.NodeService.setChildAssociationIndex=ACL_PARENT.0.sys:base.WriteProperties + org.alfresco.service.cmr.repository.NodeService.getType=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.addAspect=ACL_NODE.0.sys:base.Write + org.alfresco.service.cmr.repository.NodeService.removeAspect=ACL_NODE.0.sys:base.Write + org.alfresco.service.cmr.repository.NodeService.hasAspect=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.getAspects=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.deleteNode=ACL_NODE.0.sys:base.Delete + org.alfresco.service.cmr.repository.NodeService.addChild=ACL_NODE.0.sys:base.CreateChildren,ACL_NODE.1.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.removeChild=ACL_NODE.1.sys:base.Delete + org.alfresco.service.cmr.repository.NodeService.getProperties=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.getProperty=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.setProperties=ACL_NODE.0.sys:base.WriteProperties + org.alfresco.service.cmr.repository.NodeService.setProperty=ACL_NODE.0.sys:base.WriteProperties + org.alfresco.service.cmr.repository.NodeService.getParentAssocs=ACL_NODE.0.sys:base.ReadProperties,AFTER_ACL_PARENT.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.getChildAssocs=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.getPrimaryParent=ACL_NODE.0.sys:base.ReadProperties,AFTER_ACL_PARENT.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.createAssociation=ROLE_AUTHENTICATED + org.alfresco.service.cmr.repository.NodeService.removeAssociation=ROLE_AUTHENTICATED + org.alfresco.service.cmr.repository.NodeService.getTargetAssocs=ROLE_AUTHENTICATED + org.alfresco.service.cmr.repository.NodeService.getSourceAssocs=ROLE_AUTHENTICATED + org.alfresco.service.cmr.repository.NodeService.getPath=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.getPaths=ACL_NODE.0.sys:base.ReadProperties + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.repository.ContentService.getReader=ACL_NODE.0.cm:content.ReadContent + org.alfresco.service.cmr.repository.ContentService.getWriter=ACL_NODE.0.cm:content.WriteContent + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.search.SearchService.query=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.search.SearchService.selectNodes=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.search.SearchService.selectProperties=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.search.SearchService.contains=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.search.SearchService.like=ACL_NODE.0.sys:base.Read + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.search.CategoryService.getChildren=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.search.CategoryService.getCategories=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.search.CategoryService.getClassifications=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.search.CategoryService.getRootCategories=AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.search.CategoryService.getClassificationAspects=ACL_ALLOW + org.alfresco.service.cmr.search.CategoryService.createClassifiction=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.search.CategoryService.createRootCategory=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.search.CategoryService.createCategory=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.search.CategoryService.deleteClassification=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.search.CategoryService.deleteCategory=ACL_METHOD.ROLE_ADMINISTRATOR + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.lock.LockService.lock=ACL_NODE.0.cm:lockable.Lock + org.alfresco.service.cmr.lock.LockService.unlock=ACL_NODE.0.cm:lockable.Unlock + org.alfresco.service.cmr.lock.LockService.getLockStatus=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.lock.LockService.getLockType=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.lock.LockService.checkForLock=ACL_NODE.0.sys:base.Read + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.coci.CheckOutCheckInService.checkout=ACL_NODE.0.cm:lockable.CheckOut,ACL_NODE.1.sys:base.CreateChildren + org.alfresco.service.cmr.coci.CheckOutCheckInService.checkin=ACL_NODE.0.cm:lockable.CheckIn + org.alfresco.service.cmr.coci.CheckOutCheckInService.cancelCheckout=ACL_NODE.0.cm:lockable.CancelCheckOut + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.security.PermissionService.getOwnerAuthority=ACL_ALLOW + org.alfresco.service.cmr.security.PermissionService.getAllAuthorities=ACL_ALLOW + org.alfresco.service.cmr.security.PermissionService.getAllPermission=ACL_ALLOW + org.alfresco.service.cmr.security.PermissionService.getPermissions=ACL_NODE.0.sys:base.ReadPermissions + org.alfresco.service.cmr.security.PermissionService.getAllSetPermissions=ACL_NODE.0.sys:base.ReadPermissions + org.alfresco.service.cmr.security.PermissionService.getSettablePermissions=ACL_ALLOW + org.alfresco.service.cmr.security.PermissionService.hasPermission=ACL_ALLOW + org.alfresco.service.cmr.security.PermissionService.deletePermissions=ACL_NODE.0.sys:base.ChangePermissions + org.alfresco.service.cmr.security.PermissionService.deletePermission=ACL_NODE.0.sys:base.ChangePermissions + org.alfresco.service.cmr.security.PermissionService.setPermission=ACL_NODE.0.sys:base.ChangePermissions + org.alfresco.service.cmr.security.PermissionService.setInheritParentPermissions=ACL_NODE.0.sys:base.ChangePermissions + org.alfresco.service.cmr.security.PermissionService.getInheritParentPermissions=ACL_ALLOW + org.alfresco.service.cmr.security.PermissionService.clearPermission=ACL_NODE.0.sys:base.ChangePermissions + + + + + + + + + + + + + + + + + org.alfresco.service.cmr.security.AuthorityService.hasAdminAuthority=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.getAuthorities=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.getAllAuthorities=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.getAllRootAuthorities=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.createAuthority=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.security.AuthorityService.addAuthority=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.security.AuthorityService.removeAuthority=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.security.AuthorityService.deleteAuthority=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.security.AuthorityService.getContainedAuthorities=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.getContainingAuthorities=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.getShortName=ACL_ALLOW + org.alfresco.service.cmr.security.AuthorityService.getName=ACL_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties new file mode 100644 index 0000000000..40f20f3de5 --- /dev/null +++ b/config/alfresco/repository.properties @@ -0,0 +1,76 @@ +# Directory configuration + +dir.root=./alf_data + + +dir.contentstore=${dir.root}/contentstore + +# The location for lucene index files + +dir.indexes=${dir.root}/lucene-indexes + +# The location for lucene index locks + +dir.indexes.lock=${dir.indexes}/locks + +# #################### # +# Lucene configuration # +# #################### # +# +# The maximum number of clauses that are allowed in a lucene query +# +lucene.query.maxClauses=10000 +# +# The size of the queue of nodes waiting for index +# Events are generated as nodes are changed, this is the maximum size of the queue used to coalesce event +# When this size is reached the lists of nodes will be indexed +# +lucene.indexer.batchSize=1000 +# +# Lucene index min merge docs - the in memory size of the index +# +lucene.indexer.minMergeDocs=1000 +# +# When lucene index files are merged together - it will try to keep this number of segments/files in +# +lucene.indexer.mergeFactor=10 +# +# Roughly the maximum number of nodes indexed in one file/segment +# +lucene.indexer.maxMergeDocs=100000 +# +# The number of terms from a document that will be indexed +# +lucene.indexer.maxFieldLength=10000 + +# Database configuration + +db.driver=org.gjt.mm.mysql.Driver +db.name=alfresco +db.url=jdbc:mysql:///${db.name} +db.username=alfresco +db.password=alfresco + +# Email configuration + +mail.host=activiti2.activiti.local +mail.port=25 +mail.username=anonymous +mail.password= + +# System Configuration + +system.store=system://system +system.descriptor.childname=sys:descriptor + +# Spaces Configuration + +spaces.store=workspace://SpacesStore +spaces.company_home.childname=app:company_home +spaces.dictionary.childname=app:dictionary +spaces.templates.childname=app:space_templates +spaces.templates.content.childname=app:content_templates + +# Are user names case sensitive? + +user.name.caseSensitive=false diff --git a/config/alfresco/rule-services-context.xml b/config/alfresco/rule-services-context.xml new file mode 100644 index 0000000000..a92149b1bf --- /dev/null +++ b/config/alfresco/rule-services-context.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + alfresco.messages.rule-config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + onCreateNode + + + true + + + + + + onUpdateNode + + + + + + onDeleteNode + + + true + + + + + + onCreateChildAssociation + + + + + + onDeleteChildAssociation + + + + + + onCreateAssociation + + + + + + onDeleteAssociation + + + + + + onContentUpdate + + + + diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml new file mode 100644 index 0000000000..da3aa7db2a --- /dev/null +++ b/config/alfresco/scheduled-jobs-context.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + org.alfresco.repo.search.impl.lucene.fts.FTSIndexerJob + + + + + + + + + + + + 10000 + + + 10000 + + + + + + + + org.alfresco.util.TempFileProvider$TempFileCleanerJob + + + + + 1 + + + + + + + 1800000 + + + 3600000 + + + + + + + + org.alfresco.repo.content.ContentStoreCleanupJob + + + + + + + + + + + 24 + + + + + + + 600000 + + + 3600000 + + + + + + + + org.alfresco.repo.node.index.IndexRecoveryJob + + + + + + + + + + + + 60000 + + + 0 + + + + + + + + org.alfresco.repo.search.impl.lucene.LuceneIndexerAndSearcherFactory$LuceneIndexBackupJob + + + + + + + + + + + + + 03 + + + 00 + + + 86400000 + + + + + + + + + + + + + + + + true + + + + \ No newline at end of file diff --git a/config/alfresco/template-services-context.xml b/config/alfresco/template-services-context.xml new file mode 100644 index 0000000000..426f23ef3b --- /dev/null +++ b/config/alfresco/template-services-context.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + freeMarkerProcessor + + + + + freemarker + + + + + + + + + + + + + + diff --git a/config/alfresco/templates/content/examples/company_logos.ftl b/config/alfresco/templates/content/examples/company_logos.ftl new file mode 100644 index 0000000000..51befa00b1 --- /dev/null +++ b/config/alfresco/templates/content/examples/company_logos.ftl @@ -0,0 +1,20 @@ +<#-- Table of the images found in a folder under Company Home called "Company Logos" --> +<#-- Shows each image found as inline content --> + + <#list companyhome.children as child> + <#if child.isContainer && child.name = "Company Logos"> + <#list child.children as image> + <#switch image.mimetype> + <#case "image/jpeg"> + <#case "image/gif"> + <#case "image/png"> + + + + <#break> + <#default> + + + + +
    diff --git a/config/alfresco/templates/content/examples/doc_info.ftl b/config/alfresco/templates/content/examples/doc_info.ftl new file mode 100644 index 0000000000..859ba73dd7 --- /dev/null +++ b/config/alfresco/templates/content/examples/doc_info.ftl @@ -0,0 +1,17 @@ +<#-- Shows some general info about the current document, including NodeRef and aspects applied --> +

  • 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 z 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

    Tzbtk~S*Dm!k{(H>lPW9s|3yv@ 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