commit e1e6508fec6826261e55304d76780a9442218331 Author: Derek Hulley Date: Thu Dec 8 07:13:07 2005 +0000 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 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 0000000000..0326c06e24 Binary files /dev/null and b/config/alfresco/bootstrap/Alfresco-Tutorial.pdf differ diff --git a/config/alfresco/bootstrap/categories.xml b/config/alfresco/bootstrap/categories.xml new file mode 100644 index 0000000000..4659991bf4 --- /dev/null +++ b/config/alfresco/bootstrap/categories.xml @@ -0,0 +1,1099 @@ + + + + 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 --> +

=====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 + * + *

+ * + * 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 0000000000..0fe8759614 Binary files /dev/null and b/source/java/org/alfresco/repo/content/transform/magick/alfresco.gif differ 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 0000000000..a3bc6023f6 Binary files /dev/null and b/source/test-resources/Plan270904b.xls differ diff --git a/source/test-resources/cache-test-context.xml b/source/test-resources/cache-test-context.xml new file mode 100644 index 0000000000..43ccc50284 --- /dev/null +++ b/source/test-resources/cache-test-context.xml @@ -0,0 +1,48 @@ + + + + + + + + + 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 0000000000..f6ce700a1a Binary files /dev/null and b/source/test-resources/farmers_markets_list_2003.doc differ 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 0000000000..9883f34ea7 Binary files /dev/null and b/source/test-resources/quick/quick.bmp differ diff --git a/source/test-resources/quick/quick.doc b/source/test-resources/quick/quick.doc new file mode 100644 index 0000000000..eb307fb218 Binary files /dev/null and b/source/test-resources/quick/quick.doc differ diff --git a/source/test-resources/quick/quick.gif b/source/test-resources/quick/quick.gif new file mode 100644 index 0000000000..5e4e40e091 Binary files /dev/null and b/source/test-resources/quick/quick.gif differ 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 0000000000..08473b8e8b Binary files /dev/null and b/source/test-resources/quick/quick.jpg differ diff --git a/source/test-resources/quick/quick.odt b/source/test-resources/quick/quick.odt new file mode 100644 index 0000000000..f7eb66bea1 Binary files /dev/null and b/source/test-resources/quick/quick.odt differ diff --git a/source/test-resources/quick/quick.pdf b/source/test-resources/quick/quick.pdf new file mode 100644 index 0000000000..f7a1883f05 Binary files /dev/null and b/source/test-resources/quick/quick.pdf differ diff --git a/source/test-resources/quick/quick.png b/source/test-resources/quick/quick.png new file mode 100644 index 0000000000..8f1f89b8fc Binary files /dev/null and b/source/test-resources/quick/quick.png differ diff --git a/source/test-resources/quick/quick.ppt b/source/test-resources/quick/quick.ppt new file mode 100644 index 0000000000..5e3f1ef58b Binary files /dev/null and b/source/test-resources/quick/quick.ppt differ diff --git a/source/test-resources/quick/quick.sxw b/source/test-resources/quick/quick.sxw new file mode 100644 index 0000000000..552f28f6cd Binary files /dev/null and b/source/test-resources/quick/quick.sxw differ 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 0000000000..e6ec64cb4d Binary files /dev/null and b/source/test-resources/quick/quick.xls differ 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 +