From 8521b7d7bda6f4363b649b77b98a16e878a71e20 Mon Sep 17 00:00:00 2001 From: Dave Ward Date: Thu, 1 Mar 2012 11:41:07 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20V4.0-BUG-FIX=20to=20HEAD=20=20=20=2034?= =?UTF-8?q?060:=20Merged=20V4.0=20(4.0)=20to=204.0-BUG-FIX=20(4.0.1)=20<<>>=20=20=20=20=20=20=20330?= =?UTF-8?q?56:=20Fix=20for=20ALF-12280:=20Upgrading=20from=20version=203.4?= =?UTF-8?q?.7=20to=204.0.0=20failed=20with=20MS=20SQL=20database=20=20=20?= =?UTF-8?q?=20=20=20=20=20-=20Added=20dialect-specific=20script=20for=20SQ?= =?UTF-8?q?L=20Server=20=20=20=20=20=20=2033059:=20Fix=20for=20ALF-12127,?= =?UTF-8?q?=20ALF-11161,=20ALF-11988=20=20=20=20=20=20=20=20=20=20Merged?= =?UTF-8?q?=20BRANCHES/DEV/THOR1=20to=20BRANCHES/V4.0=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=2033049:=20Fixed=20follow=20issues=20on=20THOR-?= =?UTF-8?q?839=20&=20THOR-826=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20-=20Following=20webscripts=20now=20sets=20"Conte?= =?UTF-8?q?nt-Type"=20response=20header=20to=20application/json=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20-=20which=20?= =?UTF-8?q?makes=20people=20search=20display=20follow=20buttons=20for=20pe?= =?UTF-8?q?ople=20correctly=20=20=20=20=20=20=20Fix=20for=20ALF-12077=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20Merged=20BRANCHES/DEV/V3.4-BUG-FIX?= =?UTF-8?q?=20to=20BRANCHES/V4.0=20=20=20=20=20=20=20=20=20=20=20=20=20329?= =?UTF-8?q?99:=20Fix=20for=20ALF-12050=20-=20IE=20specific=20handling=20of?= =?UTF-8?q?=20Ajax=20requests=20does=20not=20correctly=20respect=20no-cach?= =?UTF-8?q?e=20setting,=20need=20to=20set=20Expires=20header=20also=20=20?= =?UTF-8?q?=20=20=20=20=2033060:=20Fix=20for=20ALF-12208=20-=20group=20nam?= =?UTF-8?q?e=20encoding=20=20=20=20=20=20=2033072:=20Merge=20from=20HEAD?= =?UTF-8?q?=20to=20V4.0=20=20=20=20=20=20=20=20=20=2033071:=20ALF-11843=20?= =?UTF-8?q?CLONE=20-=20Enterprise=20unlimited=20licenses=20still=20get=20i?= =?UTF-8?q?nvalidated=20turning=20the=20system=20into=20read-only=20mode?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20-=20Fixed=20build=20fail?= =?UTF-8?q?ure=20in=20HeartBeat.=20It=20had=20relied=20on=20the=20fact=20t?= =?UTF-8?q?hat=20the=20previous=20LicenseComponent=20kept=20calling=20onLi?= =?UTF-8?q?censeChange=20every=20time=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20the=20license=20was=20checked.=20It=20needed=20the=20c?= =?UTF-8?q?heck=201=20minute=20after=20the=20initial=20bootstrap=20call=20?= =?UTF-8?q?as=20there=20was=20a=20memory=20model=20sync=20issue=20in=20the?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20HeartBeat=20constr?= =?UTF-8?q?uctor=20to=20do=20with=20setting=20the=20URL=20it=20needed=20to?= =?UTF-8?q?=20call.=20=20=20=20=20=20=2033073:=20Fix=20for=20ALF-12295=20-?= =?UTF-8?q?=20CLONE=20-=20Upload=20issue=20=3F=20-=20Failed=20to=20get=20c?= =?UTF-8?q?ontent=20...=20(No=20such=20file=20or=20directory)=20...=20x22?= =?UTF-8?q?=20=20=20=20=20=20=2033083:=20Merge=20from=20HEAD=20to=20V4.0?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=2033082:=20ALF-11843=20CLONE=20-=20?= =?UTF-8?q?Enterprise=20unlimited=20licenses=20still=20get=20invalidated?= =?UTF-8?q?=20turning=20the=20system=20into=20read-only=20mode=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20-=20Did=20not=20refresh=20Tortoise?= =?UTF-8?q?=20window,=20this=20file=20was=20missed=20in=20the=20last=20com?= =?UTF-8?q?mit=20=20=20=20=20=20=20=20=20=2033080:=20ALF-11843=20CLONE=20-?= =?UTF-8?q?=20Enterprise=20unlimited=20licenses=20still=20get=20invalidate?= =?UTF-8?q?d=20turning=20the=20system=20into=20read-only=20mode=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20-=20On=20reflection=20de?= =?UTF-8?q?cided=20to=20call=20onLicenseChange=20every=20time=20the=20lice?= =?UTF-8?q?nse=20is=20checked.=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20A=20change=20of=20valid=20license=20would=20not=20h?= =?UTF-8?q?ave=20resulted=20in=20a=20call=20to=20onLicenseChange=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20Also=20have=20been?= =?UTF-8?q?=20able=20to=20make=20failure=20and=20success=20code=20more=20s?= =?UTF-8?q?ymmetrical.=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20-=20?= =?UTF-8?q?The=20previous=20commit=20also=20added=20a=20RetryingTransactio?= =?UTF-8?q?n=20around=20the=20sendData()=20call=20to=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20currentRepoDescriptorDAO.getLi?= =?UTF-8?q?censeKey()=20which=20I=20found=20while=20making=20the=20HeartBe?= =?UTF-8?q?at=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20changes?= =?UTF-8?q?.=20As=20a=20result=20we=20should=20no=20longer=20see=20the=20e?= =?UTF-8?q?rror=20in=204.0=20about=20there=20not=20being=20a=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20transaction.=20=20=20?= =?UTF-8?q?=20=20=20=2033087:=20Calendar:=20Permissions=20updates,=20fixes?= =?UTF-8?q?:=20ALF-12179=20&=20makes=20the=20permissions=20flag=20boolean.?= =?UTF-8?q?=20=20=20=20=20=20=2033088:=20FIXED=20:=20ALF-11862:=20An=20err?= =?UTF-8?q?or=20message=20appears=20when=20you=20open=20the=20"edit=20task?= =?UTF-8?q?",=20to=20request=20to=20join=20the=20"moderated=20site"=20=20?= =?UTF-8?q?=20=20=20=20=20Now=20handles=20null=20value=20=20=20=20=20=20?= =?UTF-8?q?=2033102:=20Fix=20for=20ACT=20#15024-37148=20(will=20update=20w?= =?UTF-8?q?ith=20JIRA=20no.=20once=20available)=20=20=20=20=20=20=20-=20is?= =?UTF-8?q?sue=20where=20in=20a=20load=20balanced=20Share=20environment=20?= =?UTF-8?q?(multiple=20web-tiers=20behind=20a=20reverse=20proxy)=20the=20m?= =?UTF-8?q?odification=20to=20the=20template=20layout=20selection=20for=20?= =?UTF-8?q?a=20site=20or=20user=20dashboard=20would=20not=20be=20reflected?= =?UTF-8?q?=20in=20all=20servers.=20=20=20=20=20=20=2033105:=20Bitrock=20l?= =?UTF-8?q?icense=20notice=20file.=20=20=20=20=20=20=2033114:=20Merged=20D?= =?UTF-8?q?EV=20to=20V4.0=20=20=20=20=20=20=20=20=20=2033067:=20Fix=20ALF-?= =?UTF-8?q?12206:=20CMIS:=20Error=20getting=20association=20information=20?= =?UTF-8?q?referencing=20archived=20node=20=20=20=20=20=20=2033122:=20Fix?= =?UTF-8?q?=20for=20ALF-12316=20Repo=20->=20SOLR=20query=20uses=20HTTPClie?= =?UTF-8?q?nt=20that=20only=20supports=202=20simultaneous=20connections=20?= =?UTF-8?q?=20=20=20=20=20=20-=20configurable=20via=20spring=20(default=20?= =?UTF-8?q?if=20unconfigured=20is=2040=20connections=20to=20one=20host=20a?= =?UTF-8?q?nd=2040=20max=20connections)=20=20=20=20=20=20=2033142:=20ALF-1?= =?UTF-8?q?2339:=20Prevents=20ArrayOutOfBoundsException=20that=20can=20occ?= =?UTF-8?q?ur=20with=20concurrent=20access=20of=20i18n=20bundle=20in=20Web?= =?UTF-8?q?Script=20=20=20=2034065:=20Fix=20for=20ALF-12708=20(part=202)?= =?UTF-8?q?=20=20=20=20=20=20-=20Alfresco=20opencmis=20extensions=20librar?= =?UTF-8?q?y=20=20=20=2034093:=20ALF-10902=20:=20CIFS:=20No=20friendly=20n?= =?UTF-8?q?otification=20occurs=20when=20Editor=20or=20Collaborator=20trie?= =?UTF-8?q?s=20to=20delete=20content=20=20=20=2034120:=20ALF-12767=20:=20C?= =?UTF-8?q?IFS=20TextEdit=20-=20File=20has=20been=20modified=20outside=20T?= =?UTF-8?q?extEdit=20=20=20=2034125:=20Merged=20BRANCHES\V4.0=20to=20BRANC?= =?UTF-8?q?HES\DEV\V4.0-BUG-FIX=20=20=20=20=20=20=20=2034094:=20Fix=20for?= =?UTF-8?q?=20ALF-12944=20OpenCMIS=20-=20CMIS-QL=20-=20Range=20queries=20f?= =?UTF-8?q?or=20date=20and=20datetime=20properties=20fail=20=20=20=20=20?= =?UTF-8?q?=20=20=2034095:=20Fix=20for=20ALF-12944=20OpenCMIS=20-=20CMIS-Q?= =?UTF-8?q?L=20-=20Range=20queries=20for=20date=20and=20datetime=20propert?= =?UTF-8?q?ies=20fail=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20-=20caug?= =?UTF-8?q?ht=20incorrect=20exception=20-=20so=20much=20for=20reading=20th?= =?UTF-8?q?e=20Java=20Doc=20:-)=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20-=20build=20fix=20=20=20=2034138:=20ALF-564=20:=20Is=20netwo?= =?UTF-8?q?rk-protocol-context.xml=20still=20useful=20=3F=20=20=20=2034149?= =?UTF-8?q?:=20Removes=20more=20server=20side=20rendered=20dates:=20=20=20?= =?UTF-8?q?=20Fixes:=20ALF-12965,=20ALF-12984,=20ALF-12988.=20=20=20=20341?= =?UTF-8?q?58:=20Fix=20for=20ALF-12741=20-=20Steck=20specific=20:=20error?= =?UTF-8?q?=20on=20managing=20groups=20=20=20=2034176:=20Merged=20BRANCHES?= =?UTF-8?q?\V4.0=20to=20BRANCHES\DEV\V4.0-BUG-FIX=20=20=20=20=20=20=20=203?= =?UTF-8?q?4155:=20Fix=20for=20ALF-12979=20CLONE=20-=20Search=20-=20search?= =?UTF-8?q?ing=20in=20site=20without=20any=20images=20for=20*.jpg=20brings?= =?UTF-8?q?=20back=20all=20the=20documents=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20-=20note=20this=20relies=20on=20wildcard/prefix/?= =?UTF-8?q?term/phrase=20all=20going=20through=20the=20phrase=20implementa?= =?UTF-8?q?tion=20for=20wildcard=20from=20ALF-12162=20=20=20=2034193:=20Fi?= =?UTF-8?q?x=20for=20ALF-12205=20=20=20=2034196:=20Fix=20for=20ALF-12758?= =?UTF-8?q?=20=20=20=2034201:=20ALF-12892:=20Ensure=20that=20document=20pe?= =?UTF-8?q?rmissions=20are=20refreshed=20in=20the=20dialog=20after=20being?= =?UTF-8?q?=20changed=20=20=20=2034214:=20Switched=20off=20the=20CIFS=20Ke?= =?UTF-8?q?rberos=20ticket=20cracking=20code=20by=20default,=20added=20a?= =?UTF-8?q?=20config=20value=20to=20enable=20it,=20latest=20JVMs=20do=20no?= =?UTF-8?q?t=20require=20this.=20Part=20of=20ALF-12294.=20=20=20=20CIFS=20?= =?UTF-8?q?Kerberos=20authentication=20now=20works=20with=20the=20IBM=20JD?= =?UTF-8?q?K.=20=20=20=2034215:=20Switched=20off=20the=20CIFS=20Kerberos?= =?UTF-8?q?=20ticket=20cracking=20code=20by=20default,=20added=20a=20prope?= =?UTF-8?q?rty=20to=20enable=20it,=20latest=20JVMs=20do=20not=20require=20?= =?UTF-8?q?this.=20Part=20of=20ALF-12294.=20=20=20=20CIFS=20Kerberos=20aut?= =?UTF-8?q?hentication=20now=20works=20with=20the=20IBM=20JDK=20(and=20Ope?= =?UTF-8?q?nJDK,=20Oracle/Sun=20JVMs)=20=20=20=2034219:=20Merged=20BRANCHE?= =?UTF-8?q?S/DEV/THOR1=20to=20BRANCHES/DEV/V4.0-BUG-FIX:=20=20=20=20=20=20?= =?UTF-8?q?=2032096:=20THOR-429:=20Fix=20"MT:=20Thumbnail=20+=20Preview=20?= =?UTF-8?q?are=20not=20updated=20(after=20uploading=20new=20version)"=20?= =?UTF-8?q?=20=20=20=20=20=2032125:=20THOR-429:=20Fix=20"MT:=20Thumbnail?= =?UTF-8?q?=20+=20Preview=20are=20not=20updated=20(after=20uploading=20new?= =?UTF-8?q?=20version)"=20=20=20=2034220:=20Minor:=20follow-on=20to=20r342?= =?UTF-8?q?19=20(ALF-11563)=20=20=20=2034226:=20ALF-12780:=09Mac=20OS=20X?= =?UTF-8?q?=20Lion=2010.7.2:=20Editing=20a=20document=20via=20CIFS=20and?= =?UTF-8?q?=20TextEdit=20removes=20versionable=20aspect=20from=20this=20fi?= =?UTF-8?q?le=20=20=20=2034228:=20ALF-12689:=20Fixed=20character=20encodin?= =?UTF-8?q?g=20issue=20with=20dynamic=20welcome=20dashlet=20=20=20=2034237?= =?UTF-8?q?:=20ALF-12740:=20Updated=20XHR=20requests=20to=20include=20a=20?= =?UTF-8?q?noCache=20request=20parameter=20to=20address=20IE=20issue=20whe?= =?UTF-8?q?re=20304=20reponse=20is=20assumed=20for=20XHR=20request=20=20?= =?UTF-8?q?=20=2034240:=20ALF-12835:=20Second=20click=20in=20status=20box?= =?UTF-8?q?=20no=20longer=20clears=20status=20=20=20=2034241:=20ALF-11991:?= =?UTF-8?q?=20Updated=20DocLib=20to=20support=20categories=20=20=20=203424?= =?UTF-8?q?5:=20Merged=20BRANCHES/DEV/THOR1=5FSPRINTS=20to=20BRANCHES/DEV/?= =?UTF-8?q?V4.0-BUG-FIX:=20=20=20=20=20=20=2033420:=20THOR-1000:=20Solr=20?= =?UTF-8?q?tracking:=20NodeContentGet=20should=20not=20create=20(empty)=20?= =?UTF-8?q?temp=20file=20if=20there=20is=20no=20transformer=20(eg.=20for?= =?UTF-8?q?=20image=20node)=20=20=20=2034246:=20Reverse=20merge=20of=20BRA?= =?UTF-8?q?NCHES/DEV/V4.0-BUG-FIX=20-c=2034245=20=20=20=20Due=20to=20an=20?= =?UTF-8?q?'svn=20commit'=20command=20argument=20ordering=20error,=20I=20c?= =?UTF-8?q?hecked=20in=20the=20solrcore.properties=20files.=20This=20rever?= =?UTF-8?q?se=20merge=20removes=20those=20changes.=20=20=20=2034247:=20Mer?= =?UTF-8?q?ged=20BRANCHES/DEV/THOR1=5FSPRINTS=20to=20BRANCHES/DEV/V4.0-BUG?= =?UTF-8?q?-FIX:=20=20=20=20=20=20=2033420:=20THOR-1000:=20Solr=20tracking?= =?UTF-8?q?:=20NodeContentGet=20should=20not=20create=20(empty)=20temp=20f?= =?UTF-8?q?ile=20if=20there=20is=20no=20transformer=20(eg.=20for=20image?= =?UTF-8?q?=20node)=20=20=20=2034249:=20ALF-12782=20:=20IMAP=20-=20No=20fr?= =?UTF-8?q?iendly=20notification=20occurs=20when=20a=20user=20without=20de?= =?UTF-8?q?lete=20permissions=20tries=20to=20delete=20content=20=20=20=203?= =?UTF-8?q?4254:=20Fix=20for=20ALF-13090=20SOLR=20-=20cross=20tokenisation?= =?UTF-8?q?=20field=20matches=20too=20much=20for=20"*u*a"=20=20=20=2034262?= =?UTF-8?q?:=20Fixes:=20ALF-11557:=20Publishing=20Balloon=20popups=20appea?= =?UTF-8?q?ring=20in=20wrong=20locations.=20Now=20appears=20in=20correct?= =?UTF-8?q?=20location=20in=20Doc=20Lib=20&=20replaced=20with=20standard?= =?UTF-8?q?=20popup=20message=20on=20Channel=20Admin=20page.=20=20=20=2034?= =?UTF-8?q?279:=20NodeDAO:=20re-parent=20"lost=20&=20found"=20orphan=20chi?= =?UTF-8?q?ld=20nodes=20(see=20ALF-12358=20&=20ALF-13066=20/=20SYS-301)=20?= =?UTF-8?q?=20=20=20-=20if=20orphaned=20nodes=20are=20identified=20(eg.=20?= =?UTF-8?q?via=20getPath(s))=20then=20attempt=20partial=20recovery=20by=20?= =?UTF-8?q?placing=20them=20in=20(temp)=20lost=5Ffound=20=20=20=20-=20...?= =?UTF-8?q?=20ALF-12358=20('child'=20node=20has=20deleted=20parent(s))=20?= =?UTF-8?q?=20=20=20-=20...=20ALF-13066=20(non-root=20'child'=20node=20has?= =?UTF-8?q?=20no=20parent(s))=20=20=20=20-=20for=20internal=20use=20only?= =?UTF-8?q?=20-=20allows=20index=20tracking=20(eg.=20Solr)=20to=20continue?= =?UTF-8?q?=20=20=20=20-=20precursor=20to=20fixing=20underlying=20root=20c?= =?UTF-8?q?auses=20=20=20=20-=20includes=20merge=20&=20extension=20of=20"t?= =?UTF-8?q?estConcurrentLinkToDeletedNode"=20(from=20DEV/DEREK/ALF-12358)?= =?UTF-8?q?=20=20=20=2034298:=20Merged=20V3.4-BUG-FIX=20to=20V4.0-BUG-FIX?= =?UTF-8?q?=20=20=20=20=20=20=2034068:=20Fix=20for=20ALF-342=20-=20Enterin?= =?UTF-8?q?g=20a=20search=20containing=20a=20double=20quote=20displays=20p?= =?UTF-8?q?op-up=20500=20error=20in=20OpenSearch=20JSF=20component=20=20?= =?UTF-8?q?=20=20=20=20=2034069:=20Fix=20for=20ALF-342=20-=20Completed=20f?= =?UTF-8?q?ix=20with=20additional=20encoded=20of=20output=20HTML=20=20=20?= =?UTF-8?q?=20=20=20=2034070:=20Fix=20for=20ALF-12553=20-=20Users=20are=20?= =?UTF-8?q?unable=20to=20see=20more=20than=20100=20sites=20under=20'My=20S?= =?UTF-8?q?ites'=20page.=20List=20length=20now=20configurable.=20=20=20=20?= =?UTF-8?q?=20=20=2034080:=20Fix=20for=20ALF-10306=20-=20Share=20Advanced?= =?UTF-8?q?=20search=20issue=20with=20the=20Date=20Range=20form=20values?= =?UTF-8?q?=20=20=20=20=20=20=2034107:=20Added=20missing=20jar=20lib=20to?= =?UTF-8?q?=20wcmquickstart=20and=20webeditor=20dependencies=20=20=20=20?= =?UTF-8?q?=20=20=2034114:=20Fix=20for=20ALF-10284=20-=20User=20should=20b?= =?UTF-8?q?e=20informed=20when=20user=20provides=20invalid=20credentials?= =?UTF-8?q?=20while=20opening=20document=20using=20link=20=20=20=20=20=20?= =?UTF-8?q?=2034151:=20Merged=20V3.4=20(3.4.8)=20to=20V3.4-BUG-FIX=20(3.4.?= =?UTF-8?q?9)=20=20=20=20=20=20=20=20=20=2034121:=20Merged=20BELARUS/V3.4-?= =?UTF-8?q?BUG-FIX-2012=5F01=5F26=20to=20V3.4=20(3.4.8)=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20Should=20have=20been=20done=20in=203.4.7?= =?UTF-8?q?=20in=20ALF-12174=20but=20was=20not=20found=20by=20Eclipse=20se?= =?UTF-8?q?arch=20=20=20=20=20=20=20=20=20=20=20=20=2034100:=20ALF-12948?= =?UTF-8?q?=20:=20Copyright=20year=20on=20"About=20Alfresco"=20page=20is?= =?UTF-8?q?=20out=20of=20date=20=20=20=20=20=20=20=20=20=20=20=20=20Update?= =?UTF-8?q?d=20copyright=20year=20to=202012.=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?34150:=20ALF-10976=20(relates=20to=20ALF-10412)=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20-=20Thumbnail=20mimetype=20check=20shou?= =?UTF-8?q?ld=20have=20been=20>=3D=200=20not=20>=200.=20=20=20=20=20=20=20?= =?UTF-8?q?34171:=20ALF-13016=20:=20TestModel=20class=20exits=20with=20a?= =?UTF-8?q?=20return=20code=20of=200=20even=20if=20model=20fails=20validat?= =?UTF-8?q?ion.=20=20=20=20=20=20=2034190:=20A=20modifiable=20map=20that?= =?UTF-8?q?=20protects=20and=20underlying=20map=20from=20modification=20?= =?UTF-8?q?=20=20=20=20=20=20=20-=20When=20cloning=20the=20backing=20map?= =?UTF-8?q?=20(in=20the=20event=20of=20an=20potentially-modifying=20operat?= =?UTF-8?q?ion)=20keys=20and=20values=20=20=20=20=20=20=20=20=20=20are=20s?= =?UTF-8?q?pecifically=20checked=20for=20mutability=20to=20prevent=20exces?= =?UTF-8?q?sive=20cloning.=20=20=20=20=20=20=20=20-=20Working=20towards=20?= =?UTF-8?q?fix=20for=20ALF-12855=20=20=20=20=20=20=2034191:=20Fix=20ALF-12?= =?UTF-8?q?855:=20Improvement=20for=20Lucene=20in=20memory=20sorting=20and?= =?UTF-8?q?=20improvement=20for=20nodeService.getProperty()=20=20=20=20=20?= =?UTF-8?q?=20=20=20-=20Use=20ValueProtectingMap=20when=20passing=20values?= =?UTF-8?q?=20out=20of=20the=20NodeDAO=20=20=20=20=20=20=20=20-=20Solves?= =?UTF-8?q?=20the=20problem=20of=20map=20cloning=20when=20used=20internall?= =?UTF-8?q?y=20as=20well=20as=20when=20calling=20NodeService.getProperty()?= =?UTF-8?q?=20=20=20=20=20=20=20=20-=20If=20client=20code=20retrieves=20im?= =?UTF-8?q?mutable=20values=20from=20the=20properties,=20then=20they=20wil?= =?UTF-8?q?l=20not=20be=20cloned=20=20=20=20=20=20=20=20-=20TODO:=20Specia?= =?UTF-8?q?l=20handling=20of=20entrySet()=20and=20keySet()=20methods=20(se?= =?UTF-8?q?e=20ALF-12868)=20to=20prevent=20interceptors=20from=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20triggering=20map=20cloning?= =?UTF-8?q?=20=20=20=20=20=20=2034230:=20Fixes:=20ALF-12520.=20Adds=20i18n?= =?UTF-8?q?=20strings=20for=20siteModel=20=20=20=20=20=20=2034253:=20Fix?= =?UTF-8?q?=20for=20ALF-13102=20-=20JBoss:=20Unathorized=20responce=20reci?= =?UTF-8?q?eved=20on=20a=20wcs/touch=20request=20with=20clustered=20alfres?= =?UTF-8?q?cos=20(ntlm=20sso=20enabled).=20=20=20=20=20=20=2034272:=20ALF-?= =?UTF-8?q?13136=20Merged=20V3.4.7=20(3.4.7.5)=20to=20V3.4-BUG-FIX=20(3.4.?= =?UTF-8?q?9)=20=20=20=20=20=20=20=20=20=2034267:=20ALF-12419=20"Garbage?= =?UTF-8?q?=20collector=20error"=20LockAcquisition=20on=20the=20OrphanReap?= =?UTF-8?q?er=20process=20=20=20=20=20=20=20=20=20=20=20=20=20-=20Modified?= =?UTF-8?q?=20OrphanReaper=20to=20use=20newer=20JobLockRefreshCallback.=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20Refresh=20lock=20ever?= =?UTF-8?q?y=20minute=20and=20timeout=20if=20it=20takes=20longer=20than=20?= =?UTF-8?q?an=20hour.=20=20=20=20=20=20=2034281:=20ALF-13145:=20Merged=20P?= =?UTF-8?q?ATCHES/V3.4.7=20to=20V3.4-BUG-FIX=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?34273:=20ALF-13112:=20Groups=20are=20not=20displayed=20when=206?= =?UTF-8?q?0k=20sites=20and=2060=20groups=20in=20the=20system=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20-=20Timeout=20adjustment=20approved?= =?UTF-8?q?=20by=20Kev=20and=20Erik=20=20=20=20=20=20=2034291:=20Merged=20?= =?UTF-8?q?V3.4=20to=20V3.4-BUG-FIX=20=20=20=20=20=20=20=20=20=2034197:=20?= =?UTF-8?q?ALF-12900=20Error=20occurs=20in=20My=20Documents=20dashlet=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20NodeRef=20(ScriptNode)=20pa?= =?UTF-8?q?ssed=20to=20the=20doclist.get.js=20doesn't=20have=20any=20conte?= =?UTF-8?q?nt.=20Not=20sure=20why=20yet.=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20Investigation=20continues,=20so=20there=20may=20be=20more?= =?UTF-8?q?=20changes=20to=20stop=20such=20nodes=20being=20passed=20in=20t?= =?UTF-8?q?he=20first=20place.=20=20=20=20=20=20=20=20=20=20=20=20=20NPE?= =?UTF-8?q?=20is=20as=20a=20result=20of=20having=20a=20nodeRef=20without?= =?UTF-8?q?=20content.=20It=20falls=20over=20on=20new=20code=20in=203.4.8?= =?UTF-8?q?=20for=20ALF-10976=20and=20ALF-10412.=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20Not=20too=20sure=20what=20would=20have=20happene?= =?UTF-8?q?d=20in=203.4.7,=20but=20expect=20there=20world=20have=20been=20?= =?UTF-8?q?another=20exception=20in=20the=20transformer=20code.=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20-=20Addition=20of=20defensive=20c?= =?UTF-8?q?ode=20around=20contentData=20being=20null=20and=20the=20reader?= =?UTF-8?q?=20given=20to=20the=20transformer=20being=20null.=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=2034198:=20ALF-12900=20Error=20occurs=20in=20My?= =?UTF-8?q?=20Documents=20dashlet=20=20=20=20=20=20=20=20=20=20=20=20=20-?= =?UTF-8?q?=20File=20missing=20from=20last=20commit=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=2034242:=20ALF-13078=20Copyright=20notice=20shows=20Alfr?= =?UTF-8?q?esco=20Software,=20Inc.=20=C2=A9=202005-2011=20All=20rights=20r?= =?UTF-8?q?eserved....=20should=20now=20be=20to=202012=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20-=20Should=20have=20been=20done=20in=203.4?= =?UTF-8?q?.7=20in=20ALF-12174=20but=20was=20not=20found=20by=20Eclipse=20?= =?UTF-8?q?search=20=20=20=20=20=20=20=20=20=2034265:=20Updated=20installe?= =?UTF-8?q?r=20splash=20screen=20for=202012=20(thanks=20Linton!)=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=2034282:=20ALF-13059:=20Windows=207=20specif?= =?UTF-8?q?ic:=20It's=20impossible=20to=20add=20documents=20to=20DWS=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20-=20Fix=20by=20Alex=20Malinovsky=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=2034286:=20ALF-12949:=20Merged=20V4.0=20t?= =?UTF-8?q?o=20V3.4=20=20=20=20=20=20=20=20=20=20=20=20=2034248:=20ALF-131?= =?UTF-8?q?02:=20NTLM=20on=20JBoss=20-=20Fix=20problem=20with=20Share=20SS?= =?UTF-8?q?O=20Authentication=20Filter=20corrupting=20cookie=20headers=20?= =?UTF-8?q?=20=20=20=20=20=2034292:=20Merged=20V3.4=20to=20V3.4-BUG-FIX=20?= =?UTF-8?q?(RECORD=20ONLY)=20=20=20=20=20=20=20=20=20=2034284:=20ALF-12949?= =?UTF-8?q?:=20Merged=20V3.4-BUG-FIX=20to=20V3.4=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=2034253:=20Fix=20for=20ALF-13102=20-=20Surf=20mixi?= =?UTF-8?q?ng=20up=20cookies=20for=20different=20sessions=20=20=20=2034299?= =?UTF-8?q?:=20Merged=20V4.0=20to=20V4.0-BUG-FIX=20=20=20=20=20=20=2034067?= =?UTF-8?q?:=20ALF-12423:=20Prevent=20script=20error=20on=20IE9=20=20=20?= =?UTF-8?q?=20=20=20=2034102:=20SPANISH:=20Fixes=20minor=20encoding=20erro?= =?UTF-8?q?r=20=20=20=20=20=20=2034115:=20Merged=20BRANCHES/DEV/BELARUS/V4?= =?UTF-8?q?.0-BUG-FIX-2012=5F01=5F20=20to=20BRANCHES/V4.0:=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=2034099:=20ALF-12710:=20Stack=20specific:=20It's?= =?UTF-8?q?=20impossible=20to=20log=20into=20CMIS=20Workbench=20through=20?= =?UTF-8?q?WebServices=20binding=20=20=20=20=20=20=2034156:=20Missed=20fro?= =?UTF-8?q?m=20commit=20for=20r34154=20=20=20=20=20=20=2034189:=20Fix=20fo?= =?UTF-8?q?r=20ALF-12822=20-=20Script=20error=20when=20Add=20translation?= =?UTF-8?q?=20=20=20=20=20=20=2034216:=20Fixes:=20ALF-11938=20-=20A=20dist?= =?UTF-8?q?inction=20needed=20making=20between=20the=20i18n=20labels=20for?= =?UTF-8?q?=20company=20address=20and=20personal=20address=20-=20I=20exten?= =?UTF-8?q?ded=20this=20to=20other=20company=20specific=20fields=20too.=20?= =?UTF-8?q?=20=20=20=20=20=2034238:=20ALF-12864:=20Removed=20trailing=20sp?= =?UTF-8?q?aces=20from=20installed=20jodconverter=20defaults=20=20=20=20?= =?UTF-8?q?=20=20=20-=20Stopped=20forms=20from=20recognising=20booleans=20?= =?UTF-8?q?=20=20=20=20=20=2034243:=20NFS,=20switch=20from=20read-only=20t?= =?UTF-8?q?o=20writeable=20file=20if=20write=20access=20required=20and=20c?= =?UTF-8?q?ached=20file=20was=20opened=20read-only.=20ALF-12193.=20=20=20?= =?UTF-8?q?=20=20=20=20Fix=20I/O=20error=20saving=20from=20OpenOffice=20on?= =?UTF-8?q?=20Linux.=20=20=20=20=20=20=2034263:=20Merged=20HEAD=20to=20V4.?= =?UTF-8?q?0=20=20=20=20=20=20=20=20=20=2034250:=20Fixed=20THOR-1137=20"Ma?= =?UTF-8?q?ke=20Spring=20Surf=20enable-auto-deploy-modules=20by=20default"?= =?UTF-8?q?=20=20=20=20=20=20=2034264:=20ALF-12975:=20alfresco-enterprise-?= =?UTF-8?q?4.0.1-installer-win-x64.exe=20/=20x32=20installers=20fail=20=20?= =?UTF-8?q?=20=20=20=20=20-=20Due=20to=20not=20detecting=20new=20stderr=20?= =?UTF-8?q?file=20=20=20=20=20=20=2034278:=20ALF-12763:=20Re-applied=20cha?= =?UTF-8?q?nge=20from=20ALF-7528=20after=20it=20was=20lost=20in=20r28224?= =?UTF-8?q?=20/=20ALF-5900=20=20=20=20=20=20=20=20=20=20-=20PutMethod=20wa?= =?UTF-8?q?s=20modified=20to=20use=20only=20guessed=20mime=20type=20for=20?= =?UTF-8?q?documents=20and=20completely=20ignore=20the=20Content-Type=20he?= =?UTF-8?q?ader=20from=20client.=20=20=20=2034303:=20Merged=20V4.0=20to=20?= =?UTF-8?q?V4.0-BUG-FIX=20(RECORD=20ONLY)=20=20=20=20=20=20=2033110:=20Mer?= =?UTF-8?q?ged=20BRANCHES/DEV/V4.0-BUG-FIX=20to=20BRANCHES/V4.0:=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=2033109:=20ALF-11479:=20When=20upgrading=20f?= =?UTF-8?q?rom=20Alfresco=20Community=203.4.d=20to=204.0.b,=20some=20nodes?= =?UTF-8?q?=20that=20are=20blocked=20and=20have=20versions=20fail=20after?= =?UTF-8?q?=20the=20upgrade=20=20=20=20=20=20=2033320:=20Merged=20BRANCHES?= =?UTF-8?q?\DEV\V4.0-BUG-FIX=20to=20BRANCHESV4.0=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=2033305:=20ALF-12463=20Error=20querying=20database=20was?= =?UTF-8?q?=20detected=20during=20upgrade=20process=20from=203.1=20to=204.?= =?UTF-8?q?0.0.=20=20=20=20=20=20=2033326:=20Merged=20BRANCHES/DEV/V3.4-BU?= =?UTF-8?q?G-FIX=20to=20BRANCHES/V4.0=20=20=20=20=20=20=20=20=20=20=203327?= =?UTF-8?q?7=20=20=20=20ALF-12468=20CLONE=20-=20Regression.=20Searches=20c?= =?UTF-8?q?ause=20database=20server=20to=20thrash=20CPU=20-=20ALF-12426=20?= =?UTF-8?q?=20=20=20=20=20=2033331:=20Merged=20BRANCHES\DEV\V3.4-BUG-FIX?= =?UTF-8?q?=20to=20BRANCHES\V4.0=20=20=20=20=20=20=20=20=20=20=2033301:=20?= =?UTF-8?q?ALF-12464:=20Merged=20PATCHES/V3.4.5=20to=20V3.4-BUG-FIX=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=2033299:=20ALF-12281:=20Me?= =?UTF-8?q?mory=20leak=20in=20ReferenceCountingReadOnlyIndexReaderFactory?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=2033303:=20ALF-12464:=20Merged?= =?UTF-8?q?=20PATCHES/V3.4.5=20to=20V3.4-BUG-FIX=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=2033302:=20ALF-12281:=20Correction=20to=20pr?= =?UTF-8?q?evious=20checkin=20-=20deal=20with=20the=20initial=20reference?= =?UTF-8?q?=20created=20by=20the=20constructor=20and=20cleared=20by=20clos?= =?UTF-8?q?eIfRequired()=20=20=20=20=20=20=2033398:=20Merged=20V4.0-BUG-FI?= =?UTF-8?q?X=20to=20V4.0=20=20=20=20=20=20=20=20=20=2033116:=20ALF-12517:?= =?UTF-8?q?=20Allow=20multiple=20deferred=20requests=20per=20oplock=20brea?= =?UTF-8?q?k,=20next=20level=20of=20fix=20for=20ALF-11935.=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=2033147:=20FTP=20implemented=20set=20modification?= =?UTF-8?q?=20date/time=20command=20(MFMT).=20ALF-12105.=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=2033151:=20Fix=20problems=20with=20FTP=20and=20UTF-8.?= =?UTF-8?q?=20JLAN-81.=20=20=20=20=20=20=20=20=20=20When=20using=20the=20J?= =?UTF-8?q?ava6=20Normalizer=20use=20the=20NFC=20form.=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=2033158:=20Fix=20NFS=20server=20swallows=20exceptions?= =?UTF-8?q?.=20ALF-11667.=20=20=20=20=20=20=20=20=20=20Startup=20exception?= =?UTF-8?q?=20details=20are=20now=20saved.=20=20=20=20=20=20=20=20=20=2033?= =?UTF-8?q?183:=20Minor=20fix=20to=20exception=20string=20in=20extendBuffe?= =?UTF-8?q?r().=20=20=20=20=20=20=2034061:=20Merged=20V4.0-BUG-FIX=20to=20?= =?UTF-8?q?V4.0=20(Start=20of=204.0.1)=20=20=20=20=20=20=2034062:=20Merge?= =?UTF-8?q?=20V4.0-BUG-FIX=20to=20V4.0=20RECORD=20ONLY=20(changes=20that?= =?UTF-8?q?=20came=20from=20V4.0)=20=20=20=20=20=20=2034109:=20Merged=20BR?= =?UTF-8?q?ANCHES/DEV/V4.0-BUG-FIX=20to=20BRANCHES/V4.0=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=2034108:=20Merged=20BRANCHES/DEV/V3.4-BUG-FIX=20to=20?= =?UTF-8?q?BRANCHES/DEV/V4.0-BUG-FIX=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20Added=20missing=20jar=20lib=20to=20wcmquickstart=20and=20web?= =?UTF-8?q?editor=20dependencies=20=20=20=20=20=20=2034154:=20Merged=20BRA?= =?UTF-8?q?NCHES/DEV/V4.0-BUG-FIX/=20to=20BRANCHES/V4.0:=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=2034149:=20Removes=20more=20server=20side=20rendered?= =?UTF-8?q?=20dates:=20Fixes:=20ALF-12965,=20ALF-12984,=20ALF-12988.=20=20?= =?UTF-8?q?=20=20=20=20=2034274:=20Merged=20V4.0-BUG-FIX=20to=20V4.0=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=2034237:=20ALF-12740:=20Updated=20XHR=20r?= =?UTF-8?q?equests=20to=20include=20a=20noCache=20request=20parameter=20to?= =?UTF-8?q?=20address=20IE=20issue=20where=20304=20reponse=20is=20assumed?= =?UTF-8?q?=20for=20XHR=20request=20=20=20=20=20=20=2034288:=20Merged=20V3?= =?UTF-8?q?.4=20to=20V4.0=20=20=20=20=20=20=20=20=20=2034197:=20ALF-12900?= =?UTF-8?q?=20Error=20occurs=20in=20My=20Documents=20dashlet=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20NodeRef=20(ScriptNode)=20passed=20to?= =?UTF-8?q?=20the=20doclist.get.js=20doesn't=20have=20any=20content.=20Not?= =?UTF-8?q?=20sure=20why=20yet.=20=20=20=20=20=20=20=20=20=20=20=20=20Inve?= =?UTF-8?q?stigation=20continues,=20so=20there=20may=20be=20more=20changes?= =?UTF-8?q?=20to=20stop=20such=20nodes=20being=20passed=20in=20the=20first?= =?UTF-8?q?=20place.=20=20=20=20=20=20=20=20=20=20=20=20=20NPE=20is=20as?= =?UTF-8?q?=20a=20result=20of=20having=20a=20nodeRef=20without=20content.?= =?UTF-8?q?=20It=20falls=20over=20on=20new=20code=20in=203.4.8=20for=20ALF?= =?UTF-8?q?-10976=20and=20ALF-10412.=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20Not=20too=20sure=20what=20would=20have=20happened=20in=203.4?= =?UTF-8?q?.7,=20but=20expect=20there=20world=20have=20been=20another=20ex?= =?UTF-8?q?ception=20in=20the=20transformer=20code.=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20-=20Addition=20of=20defensive=20code=20around?= =?UTF-8?q?=20contentData=20being=20null=20and=20the=20reader=20given=20to?= =?UTF-8?q?=20the=20transformer=20being=20null.=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=2034198:=20ALF-12900=20Error=20occurs=20in=20My=20Documents=20?= =?UTF-8?q?dashlet=20=20=20=20=20=20=20=20=20=20=20=20=20-=20File=20missin?= =?UTF-8?q?g=20from=20last=20commit=20=20=20=20=20=20=20=20=20=2034242:=20?= =?UTF-8?q?ALF-13078=20Copyright=20notice=20shows=20Alfresco=20Software,?= =?UTF-8?q?=20Inc.=20=C2=A9=202005-2011=20All=20rights=20reserved....=20sh?= =?UTF-8?q?ould=20now=20be=20to=202012=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20-=20Should=20have=20been=20done=20in=203.4.7=20in=20ALF-1217?= =?UTF-8?q?4=20but=20was=20not=20found=20by=20Eclipse=20search=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=2034265:=20Updated=20installer=20splash=20scree?= =?UTF-8?q?n=20for=202012=20(thanks=20Linton!)=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=2034284:=20ALF-12949:=20Merged=20V3.4-BUG-FIX=20to=20V3.4=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=2034253:=20Fix=20for=20ALF-13?= =?UTF-8?q?102=20-=20Surf=20mixing=20up=20cookies=20for=20different=20sess?= =?UTF-8?q?ions=20=20=20=20=20=20=20=20=20=2034286:=20ALF-12949:=20Merged?= =?UTF-8?q?=20V4.0=20to=20V3.4=20=20=20=20=20=20=20=20=20=20=20=20=2034248?= =?UTF-8?q?:=20ALF-13102:=20NTLM=20on=20JBoss=20-=20Fix=20problem=20with?= =?UTF-8?q?=20Share=20SSO=20Authentication=20Filter=20corrupting=20cookie?= =?UTF-8?q?=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@34305 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/avm-services-context.xml | 6 + config/alfresco/core-services-context.xml | 4 +- .../extension/file-servers-custom.xml.sample | 94 ---- .../extension/file-servers-custom.xml.sample2 | 61 --- .../network-protocol-context.xml.sample | 18 - .../node-common-SqlMap.xml | 12 + .../query-test-common-SqlMap.xml | 27 ++ .../messages/content-model.properties | 3 + .../alfresco/messages/site-model.properties | 5 + config/alfresco/model/systemModel.xml | 20 +- config/alfresco/mt/mt-base-context.xml | 1 + config/alfresco/repository.properties | 4 + config/alfresco/site-services-context.xml | 6 + .../kerberos-authentication-context.xml | 3 + .../kerberos-authentication.properties | 1 + .../default/network-protocol-context.xml | 6 + config/alfresco/thumbnail-service-context.xml | 1 + .../renditions/CMISRenditionServiceTest.java | 3 +- .../cifs/EnterpriseCifsAuthenticator.java | 24 +- .../filesys/repo/CommandExecutorImpl.java | 7 + .../filesys/repo/ContentDiskDriver2.java | 2 +- .../filesys/repo/ContentDiskDriverTest.java | 190 ++++++++ ...NonTransactionalRuleContentDiskDriver.java | 28 +- .../filesys/repo/TempNetworkFile.java | 3 + .../filesys/repo/rules/RuleEvaluator.java | 10 +- .../rules/ScenarioLockedDeleteShuffle.java | 110 +++++ .../ScenarioLockedDeleteShuffleInstance.java | 283 +++++++++++ .../ScenarioSimpleNonBufferedInstance.java | 2 +- .../repo/rules/commands/MoveFileCommand.java | 48 +- .../rules/operations/DeleteFileOperation.java | 6 + .../rules/operations/MoveFileOperation.java | 49 +- .../org/alfresco/repo/avm/OrphanReaper.java | 411 +++++++++------- .../repo/calendar/CalendarServiceImpl.java | 2 +- .../repo/content/ContentServiceImpl.java | 4 + .../alfresco/repo/dictionary/TestModel.java | 18 +- .../repo/domain/node/AbstractNodeDAOImpl.java | 244 ++++++++-- .../repo/domain/node/NodePropertyValue.java | 28 ++ .../NonRootNodeWithoutParentsException.java | 44 ++ .../domain/node/NotLiveNodeException.java | 44 ++ .../alfresco/repo/imap/ImapServiceImpl.java | 23 +- .../org/alfresco/repo/jscript/ScriptNode.java | 5 + .../filefolder/FileFolderServiceImpl.java | 6 +- .../filefolder/FileFolderServiceImplTest.java | 42 ++ .../repo/model/filefolder/testModel.xml | 51 ++ .../alfresco/repo/node/NodeServiceTest.java | 309 +++++++++++- .../getchildren/FilterSortNodeEntity.java | 12 + .../getchildren/GetChildrenCannedQuery.java | 12 +- .../GetChildrenCannedQueryFactory.java | 11 +- .../GetChildrenCannedQueryParams.java | 10 +- .../GetChildrenCannedQueryTest.java | 121 ++++- .../repo/node/getchildren/testModel.xml | 51 ++ ...AbstractTransformationRenderingEngine.java | 3 +- .../impl/lucene/ADMLuceneIndexerImpl.java | 6 +- .../impl/lucene/AVMLuceneIndexerImpl.java | 6 +- .../security/person/PersonServiceImpl.java | 2 +- .../alfresco/repo/site/SiteServiceImpl.java | 2 +- .../repo/template/BaseContentNode.java | 6 +- .../repo/tenant/MultiTAdminServiceImpl.java | 14 +- .../repo/thumbnail/SimpleThumbnailer.java | 75 +-- .../repo/thumbnail/ThumbnailRegistry.java | 103 ++-- .../org/alfresco/util/ValueProtectingMap.java | 441 ++++++++++++++++++ .../alfresco/util/ValueProtectingMapTest.java | 242 ++++++++++ 62 files changed, 2859 insertions(+), 526 deletions(-) delete mode 100644 config/alfresco/extension/file-servers-custom.xml.sample delete mode 100644 config/alfresco/extension/file-servers-custom.xml.sample2 delete mode 100644 config/alfresco/extension/network-protocol-context.xml.sample create mode 100644 config/alfresco/messages/site-model.properties create mode 100644 source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java create mode 100644 source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffleInstance.java create mode 100644 source/java/org/alfresco/repo/domain/node/NonRootNodeWithoutParentsException.java create mode 100644 source/java/org/alfresco/repo/domain/node/NotLiveNodeException.java create mode 100644 source/java/org/alfresco/repo/model/filefolder/testModel.xml create mode 100644 source/java/org/alfresco/repo/node/getchildren/testModel.xml create mode 100644 source/java/org/alfresco/util/ValueProtectingMap.java create mode 100644 source/java/org/alfresco/util/ValueProtectingMapTest.java diff --git a/config/alfresco/avm-services-context.xml b/config/alfresco/avm-services-context.xml index 8977a758df..a002ec43c2 100644 --- a/config/alfresco/avm-services-context.xml +++ b/config/alfresco/avm-services-context.xml @@ -73,6 +73,12 @@ + + ${orphanReaper.lockRefreshTime} + + + ${orphanReaper.lockTimeOut} + diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 2f1726e72d..9d4e82b90a 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -662,7 +662,9 @@ cm:likesRatingSchemeTotal cm:likesRatingSchemeCount cm:fiveStarRatingSchemeCount - cm:fiveStarRatingSchemeTotal + cm:fiveStarRatingSchemeTotal + + fm:commentCount diff --git a/config/alfresco/extension/file-servers-custom.xml.sample b/config/alfresco/extension/file-servers-custom.xml.sample deleted file mode 100644 index ae418494ee..0000000000 --- a/config/alfresco/extension/file-servers-custom.xml.sample +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - workspace://SpacesStore - /app:company_home - - - - __Alfresco.url - http://${localname}:8080/alfresco/ - - - - - - - - - - - - - - - - - - alfresco/desktop/Alfresco.exe - http://${localname}:8080/alfresco/ - - - org.alfresco.filesys.repo.desk.CheckInOutDesktopAction - CheckInOut - __CheckInOut.exe - - - org.alfresco.filesys.repo.desk.JavaScriptDesktopAction - JavaScriptURL - __ShowDetails.exe - - anyFiles - copyToTarget - - - org.alfresco.filesys.repo.desk.EchoDesktopAction - Echo - __AlfrescoEcho.exe - - - org.alfresco.filesys.repo.desk.URLDesktopAction - URL - __AlfrescoURL.exe - - - org.alfresco.filesys.repo.desk.CmdLineDesktopAction - CmdLine - __AlfrescoCmd.exe - - - org.alfresco.filesys.repo.desk.JavaScriptDesktopAction - JavaScript - __AlfrescoScript.exe - - anyFiles, multiplePaths , allowNoParams - confirm, copyToTarget - - - - - - - - - - - - - - - - - diff --git a/config/alfresco/extension/file-servers-custom.xml.sample2 b/config/alfresco/extension/file-servers-custom.xml.sample2 deleted file mode 100644 index 0fa89c5895..0000000000 --- a/config/alfresco/extension/file-servers-custom.xml.sample2 +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - workspace://SpacesStore - /app:company_home - - - - __Alfresco.url - http://${localname}:8080/alfresco/ - - - - - - - - - - alfresco/desktop/Alfresco.exe - http://${localname}:8080/alfresco/ - - - org.alfresco.filesys.repo.desk.CheckInOutDesktopAction - CheckInOut - __CheckInOut.exe - - - org.alfresco.filesys.repo.desk.JavaScriptDesktopAction - JavaScriptURL - __ShowDetails.exe - - anyFiles - copyToTarget - - - - - - - - - - - - - - - - diff --git a/config/alfresco/extension/network-protocol-context.xml.sample b/config/alfresco/extension/network-protocol-context.xml.sample deleted file mode 100644 index d2b30f9cb4..0000000000 --- a/config/alfresco/extension/network-protocol-context.xml.sample +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - classpath:alfresco/file-servers.xml - - classpath:alfresco/extension/file-servers-custom.xml - - - - - \ No newline at end of file diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml index 9289457fe4..be9979d71c 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml @@ -1069,6 +1069,12 @@ and prop4.string_value like #{pattern} + + and assoc.type_qname_id in + + #{item} + + @@ -1097,6 +1103,12 @@ #{item} + + and assoc.type_qname_id in + + #{item} + + and prop4.string_value like #{pattern} diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-test-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-test-common-SqlMap.xml index 6c4569501f..07d81ab68b 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-test-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-test-common-SqlMap.xml @@ -55,5 +55,32 @@ JUNKED + + + + \ No newline at end of file diff --git a/config/alfresco/messages/content-model.properties b/config/alfresco/messages/content-model.properties index 916ec5fa8d..454cfb9a77 100644 --- a/config/alfresco/messages/content-model.properties +++ b/config/alfresco/messages/content-model.properties @@ -358,3 +358,6 @@ cm_contentmodel.property.cm_isIndexed.title=Is Indexed cm_contentmodel.property.cm_isIndexed.description=Is the node indexed and can be found via search. cm_contentmodel.property.cm_isContentIndexed.title=Is Content Indexed cm_contentmodel.property.cm_isContentIndexed.description=Are the node's d:content properties indexed? + +cm_contentmodel.property.cm_tagScopeSummary.title=Tag Summary +cm_contentmodel.property.cm_tagScopeSummary.description=Tag Summary \ No newline at end of file diff --git a/config/alfresco/messages/site-model.properties b/config/alfresco/messages/site-model.properties new file mode 100644 index 0000000000..dd39280d29 --- /dev/null +++ b/config/alfresco/messages/site-model.properties @@ -0,0 +1,5 @@ +# Display labels for Site Model +st_siteModel.property.st_sitePreset.title=Site Preset +st_siteModel.property.st_sitePreset.description=Site Preset +st_siteModel.property.st_siteVisibility.title=Site Visibility +st_siteModel.property.st_siteVisibility.description=Site Visibility \ No newline at end of file diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml index d67a5b859c..844f9388a3 100644 --- a/config/alfresco/model/systemModel.xml +++ b/config/alfresco/model/systemModel.xml @@ -96,11 +96,24 @@ Store Root sys:container + + + + false + false + + + sys:lost_found + false + false + + + sys:aspect_root - + Reference sys:base @@ -112,6 +125,11 @@ + + Lost+Found + sys:container + + diff --git a/config/alfresco/mt/mt-base-context.xml b/config/alfresco/mt/mt-base-context.xml index 44f6268918..ef86911e60 100644 --- a/config/alfresco/mt/mt-base-context.xml +++ b/config/alfresco/mt/mt-base-context.xml @@ -33,6 +33,7 @@ ${alfresco_user_store.adminusername} + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index d2d4379566..9e4b297610 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -789,6 +789,10 @@ deployment.filesystem.default.rootdir=./www deployment.filesystem.default.name=filesystem deployment.filesystem.default.metadatadir=${deployment.filesystem.metadatadir}/default +# OrphanReaper +orphanReaper.lockRefreshTime=60000 +orphanReaper.lockTimeOut=3600000 + # # Encryption properties # diff --git a/config/alfresco/site-services-context.xml b/config/alfresco/site-services-context.xml index c62b35306f..023c023a1b 100644 --- a/config/alfresco/site-services-context.xml +++ b/config/alfresco/site-services-context.xml @@ -9,6 +9,12 @@ alfresco/model/siteModel.xml + + + + alfresco/messages/site-model + + diff --git a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml index 2ba2255825..a8aac32ac8 100644 --- a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml @@ -76,6 +76,9 @@ true + + ${kerberos.authentication.cifs.enableTicketCracking} + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties index 00198ffeb0..423defd9a3 100644 --- a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties +++ b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties @@ -3,4 +3,5 @@ kerberos.authentication.user.configEntryName=Alfresco kerberos.authentication.defaultAdministratorUserNames= kerberos.authentication.cifs.configEntryName=AlfrescoCIFS kerberos.authentication.cifs.password=secret +kerberos.authentication.cifs.enableTicketCracking=false kerberos.authentication.authenticateCIFS=true \ No newline at end of file diff --git a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml index 6f54d94a55..36577b79a3 100644 --- a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml +++ b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml @@ -144,6 +144,12 @@ + + + ^\._.* + 30000 + HIGH + [0-9A-F]{8}+$ diff --git a/config/alfresco/thumbnail-service-context.xml b/config/alfresco/thumbnail-service-context.xml index 6531b08cab..a6dc5246d6 100644 --- a/config/alfresco/thumbnail-service-context.xml +++ b/config/alfresco/thumbnail-service-context.xml @@ -58,6 +58,7 @@ + diff --git a/source/java/org/alfresco/cmis/renditions/CMISRenditionServiceTest.java b/source/java/org/alfresco/cmis/renditions/CMISRenditionServiceTest.java index 973514066d..a609b553c1 100644 --- a/source/java/org/alfresco/cmis/renditions/CMISRenditionServiceTest.java +++ b/source/java/org/alfresco/cmis/renditions/CMISRenditionServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -199,6 +199,7 @@ public class CMISRenditionServiceTest extends BaseCMISTest contentWriter.setLocale(Locale.ENGLISH); contentWriter.putContent(documentContent); ContentReader contentReader = fileFolderService.getReader(textDocument); + // contentReader will not be null as an exception will have been thrown if there was a problem NodeRef document = fileFolderService.create(rootNodeRef, documentName, ContentModel.TYPE_CONTENT).getNodeRef(); contentWriter = fileFolderService.getWriter(document); diff --git a/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java b/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java index 362246174f..0c2c9f4196 100644 --- a/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java +++ b/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java @@ -137,9 +137,17 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement private byte[] m_negTokenInit; private String m_mecListMIC; + // Enable Kerberos debug output + private boolean kerberosDebug; + // Disable NTLM logons, only Kerberos logons allowed + private boolean disableNTLM; + + // Enable ticket cracking code, required for Java5 JVMs + + private boolean m_enableTicketCracking; /** * Class constructor @@ -202,6 +210,20 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement this.m_acceptNTLMv1 = !disallowNTLMv1; } + /** + * Enable Kerbeors ticket cracking code that is required for Java5 + * + * @param enaTktCracking boolean + */ + public void setEnableTicketCracking( boolean enaTktCracking) { + m_enableTicketCracking = enaTktCracking; + + // Debug + + if ( logger.isInfoEnabled() && enaTktCracking) + logger.info("CIFS Kerberos authentication, ticket cracking enabled (for mutual authentication)"); + } + /** * Initialize the authenticator (via the config service) * @@ -1372,7 +1394,7 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement KrbAuthContext krbAuthCtx = null; - if ( krbApReq.hasMutualAuthentication()) + if ( krbApReq.hasMutualAuthentication() && m_enableTicketCracking == true) { // Allocate the Kerberos authentication and parse the AP-REQ diff --git a/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java b/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java index 241bf6e91c..55207718de 100644 --- a/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java +++ b/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java @@ -15,6 +15,7 @@ import org.alfresco.filesys.repo.rules.commands.CopyContentCommand; import org.alfresco.filesys.repo.rules.commands.CreateFileCommand; import org.alfresco.filesys.repo.rules.commands.DeleteFileCommand; import org.alfresco.filesys.repo.rules.commands.DoNothingCommand; +import org.alfresco.filesys.repo.rules.commands.MoveFileCommand; import org.alfresco.filesys.repo.rules.commands.OpenFileCommand; import org.alfresco.filesys.repo.rules.commands.ReduceQuotaCommand; import org.alfresco.filesys.repo.rules.commands.RemoveNoContentFileOnError; @@ -248,6 +249,12 @@ public class CommandExecutorImpl implements CommandExecutor RenameFileCommand rename = (RenameFileCommand)command; diskInterface.renameFile(sess, tree, rename.getFromPath(), rename.getToPath()); } + else if(command instanceof MoveFileCommand) + { + logger.debug("move command"); + MoveFileCommand rename = (MoveFileCommand)command; + diskInterface.renameFile(sess, tree, rename.getFromPath(), rename.getToPath()); + } else if(command instanceof CopyContentCommand) { if(logger.isDebugEnabled()) diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java index c5f3501629..41b86929ec 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java @@ -1489,7 +1489,7 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD // Check for delete permission if ( permissionService.hasPermission(nodeRef, PermissionService.DELETE) == AccessStatus.DENIED) { - throw new AccessDeniedException("No delete access to :" + name); + throw new PermissionDeniedException("No delete access to :" + name); } // Check if the node is locked diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java index 92fd2039c7..e820644f99 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java @@ -4341,6 +4341,196 @@ public class ContentDiskDriverTest extends TestCase } // test set modified scenario + /** + * This test tries to simulate the cifs shuffling that is done + * from Save from Mac Lion by TextEdit + * + * a) Lock file created. (._test.txt) + * b) Temp file created in temporary folder (test.txt) + * c) Target file deleted + * d) Temp file renamed to target file. + * e) Lock file deleted + * + */ + public void testScenarioMacLionTextEdit() throws Exception + { + logger.debug("testScenarioLionTextEdit"); + final String FILE_NAME = "test.txt"; + final String LOCK_FILE_NAME = "._test.txt"; + final String TEMP_FILE_NAME = "test.txt"; + + final String UPDATED_TEXT = "Mac Lion Text Updated Content"; + + class TestContext + { + NetworkFile lockFileHandle; + NetworkFile firstFileHandle; + NetworkFile tempFileHandle; + NodeRef testNodeRef; // node ref of test.doc + }; + + final TestContext testContext = new TestContext(); + + final String TEST_ROOT_DIR = "\\ContentDiskDriverTest"; + final String TEST_DIR = "\\ContentDiskDriverTest\\testScenarioLionTextEdit"; + final String TEST_TEMP_DIR = "\\ContentDiskDriverTest\\testScenarioLionTextEdit\\.Temporary Items"; + + ServerConfiguration scfg = new ServerConfiguration("testServer"); + TestServer testServer = new TestServer("testServer", scfg); + final SrvSession testSession = new TestSrvSession(666, testServer, "test", "remoteName"); + DiskSharedDevice share = getDiskSharedDevice(); + final TreeConnection testConnection = testServer.getTreeConnection(share); + final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper(); + + /** + * Create a file in the test directory + */ + RetryingTransactionCallback createFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + /** + * Create the test directory we are going to use + */ + FileOpenParams createRootDirParams = new FileOpenParams(TEST_ROOT_DIR, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + FileOpenParams createDirParams = new FileOpenParams(TEST_DIR, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + FileOpenParams createTempDirParams = new FileOpenParams(TEST_TEMP_DIR, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + driver.createDirectory(testSession, testConnection, createRootDirParams); + driver.createDirectory(testSession, testConnection, createDirParams); + driver.createDirectory(testSession, testConnection, createTempDirParams); + + /** + * Create the file we are going to use + */ + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams); + assertNotNull(testContext.firstFileHandle); + + String testContent = "Mac Lion Text"; + byte[] testContentBytes = testContent.getBytes(); + + driver.writeFile(testSession, testConnection, testContext.firstFileHandle, testContentBytes, 0, testContentBytes.length, 0); + driver.closeFile(testSession, testConnection, testContext.firstFileHandle); + + /** + * Create the temp file we are going to use + */ + FileOpenParams createTempFileParams = new FileOpenParams(TEST_TEMP_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.tempFileHandle = driver.createFile(testSession, testConnection, createTempFileParams); + assertNotNull(testContext.tempFileHandle); + + testContent = UPDATED_TEXT; + testContentBytes = testContent.getBytes(); + driver.writeFile(testSession, testConnection, testContext.tempFileHandle, testContentBytes, 0, testContentBytes.length, 0); + driver.closeFile(testSession, testConnection, testContext.tempFileHandle); + + return null; + } + }; + tran.doInTransaction(createFileCB, false, true); + + /** + * a) create the lock file + */ + RetryingTransactionCallback createLockFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + /** + * Create the lock file we are going to use + */ + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + LOCK_FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.lockFileHandle = driver.createFile(testSession, testConnection, createFileParams); + assertNotNull(testContext.lockFileHandle); + testContext.lockFileHandle.closeFile(); + + /** + * Also add versionable to target file + */ + testContext.testNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + nodeService.addAspect(testContext.testNodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + + return null; + } + }; + tran.doInTransaction(createLockFileCB, false, true); + + /** + * b) Delete the target file + */ + RetryingTransactionCallback deleteTargetFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + tran.doInTransaction(deleteTargetFileCB, false, true); + + /** + * c) Move the temp file into place + */ + RetryingTransactionCallback moveTempFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.renameFile(testSession, testConnection, TEST_TEMP_DIR + "\\" + TEMP_FILE_NAME, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + tran.doInTransaction(moveTempFileCB, false, true); + + + /** + * d) Delete Lock File + */ + RetryingTransactionCallback deleteLockFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + LOCK_FILE_NAME); + + return null; + } + }; + + tran.doInTransaction(deleteLockFileCB, false, true); + + RetryingTransactionCallback validateCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + + NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + + assertEquals("shuffledNode ref is different", shuffledNodeRef, testContext.testNodeRef); + assertTrue("", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); + + ContentReader reader = contentService.getReader(shuffledNodeRef, ContentModel.PROP_CONTENT); + assertNotNull("Reader is null", reader); + String s = reader.getContentString(); + assertEquals("content not written", UPDATED_TEXT, s); + + + return null; + } + }; + + tran.doInTransaction(validateCB, false, true); + + } // testScenarioLionTextEdit + + + + /** * Test server */ diff --git a/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java index ae4cb1a3c4..3a200a445f 100644 --- a/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/NonTransactionalRuleContentDiskDriver.java @@ -34,6 +34,7 @@ import org.alfresco.filesys.repo.rules.RuleEvaluator; import org.alfresco.filesys.repo.rules.operations.CloseFileOperation; import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; import org.alfresco.filesys.repo.rules.operations.DeleteFileOperation; +import org.alfresco.filesys.repo.rules.operations.MoveFileOperation; import org.alfresco.filesys.repo.rules.operations.OpenFileOperation; import org.alfresco.filesys.repo.rules.operations.RenameFileOperation; import org.alfresco.jlan.server.SrvSession; @@ -430,17 +431,24 @@ public class NonTransactionalRuleContentDiskDriver implements ExtendedDiskInterf } else { - logger.debug("move - call renameFile directly"); -// // TODO Use old interface for rename/move until think -// // through move operation and how it applies to the evaluator contexts -// // plural since there will be two contexts. -// logger.debug("move"); -// Operation o = new MoveFileOperation(oldFile, newFile); -// Command c = ruleEvaluator.evaluate(ctx, o); -// -// commandExecutor.execute(sess, tree, c); + logger.debug("moveFileCommand - move between folders"); + + Operation o = new MoveFileOperation(oldFile, newFile, oldPath, newPath, rootNode); - diskInterface.renameFile(sess, tree, oldPath, newPath); + /* + * Note: At the moment we only have move scenarios for the destination folder - so + * we only need to evaluate against a single (destination) context/folder. + * This will require re-design as and when we need to have scenarios for the source/folder + */ + + //EvaluatorContext ctx1 = getEvaluatorContext(driverState, oldFolder); + EvaluatorContext ctx2 = getEvaluatorContext(driverState, newFolder); + + Command c = ruleEvaluator.evaluate(ctx2, o); + + commandExecutor.execute(sess, tree, c); + + // diskInterface.renameFile(sess, tree, oldPath, newPath); } diff --git a/source/java/org/alfresco/filesys/repo/TempNetworkFile.java b/source/java/org/alfresco/filesys/repo/TempNetworkFile.java index 135a9a4e71..62d4192f21 100644 --- a/source/java/org/alfresco/filesys/repo/TempNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/TempNetworkFile.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.io.Reader; +import org.alfresco.jlan.server.filesys.FileAttribute; import org.alfresco.jlan.server.filesys.cache.FileState; import org.alfresco.jlan.server.filesys.cache.NetworkFileStateInterface; import org.alfresco.jlan.smb.server.disk.JavaNetworkFile; @@ -27,6 +28,7 @@ public class TempNetworkFile extends JavaNetworkFile implements NetworkFileState { super(file, netPath); setFullName(netPath); + setAttributes(FileAttribute.NTNormal); } /** @@ -39,6 +41,7 @@ public class TempNetworkFile extends JavaNetworkFile implements NetworkFileState { super(file, netPath); setFullName(netPath); + setAttributes(FileAttribute.NTNormal); } /** diff --git a/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java b/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java index d420ee37a5..0520179c23 100644 --- a/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java +++ b/source/java/org/alfresco/filesys/repo/rules/RuleEvaluator.java @@ -34,8 +34,12 @@ public interface RuleEvaluator public EvaluatorContext createContext(); /** - * Evaluate the scenarios against the current operation - * @param Command the command to fulfill the operation + * Evaluate the scenarios contained within the context against the current operation + * @param context - the context to evaluate the operation + * @param operation - the operation to be evaluated. + * @return Command the command to fulfil the operation */ - public Command evaluate(EvaluatorContext context, Operation operation); + public Command evaluate(EvaluatorContext context, Operation operation); + + } diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java new file mode 100644 index 0000000000..d9568aa8e7 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffle.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.filesys.repo.rules; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.filesys.repo.rules.ScenarioInstance.Ranking; +import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A locked delete shuffle + */ +public class ScenarioLockedDeleteShuffle implements Scenario +{ + private static Log logger = LogFactory.getLog(ScenarioLockedDeleteShuffle.class); + + /** + * The regex pattern of a create that will trigger a new instance of + * the scenario. + */ + private Pattern pattern; + private String strPattern; + + + private long timeout = 30000; + + private Ranking ranking = Ranking.HIGH; + + @Override + public ScenarioInstance createInstance(final List currentInstances, Operation operation) + { + /** + * This scenario is triggered by a create of a file matching + * the pattern + */ + if(operation instanceof CreateFileOperation) + { + CreateFileOperation c = (CreateFileOperation)operation; + + Matcher m = pattern.matcher(c.getName()); + if(m.matches()) + { + if(logger.isDebugEnabled()) + { + logger.debug("New Scenario Locked Delete Shuffle Instance pattern:" + strPattern); + } + + ScenarioLockedDeleteShuffleInstance instance = new ScenarioLockedDeleteShuffleInstance() ; + instance.setTimeout(timeout); + instance.setRanking(ranking); + return instance; + } + } + + // No not interested. + return null; + + } + + public void setPattern(String pattern) + { + this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + this.strPattern = pattern; + } + + public String getPattern() + { + return this.strPattern; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public long getTimeout() + { + return timeout; + } + + public void setRanking(Ranking ranking) + { + this.ranking = ranking; + } + + public Ranking getRanking() + { + return ranking; + } +} diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffleInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffleInstance.java new file mode 100644 index 0000000000..d83ad8d0c3 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioLockedDeleteShuffleInstance.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.filesys.repo.rules; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.filesys.repo.rules.commands.CompoundCommand; +import org.alfresco.filesys.repo.rules.commands.CopyContentCommand; +import org.alfresco.filesys.repo.rules.commands.DeleteFileCommand; +import org.alfresco.filesys.repo.rules.commands.RenameFileCommand; +import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; +import org.alfresco.filesys.repo.rules.operations.DeleteFileOperation; +import org.alfresco.filesys.repo.rules.operations.MoveFileOperation; +import org.alfresco.filesys.repo.rules.operations.RenameFileOperation; +import org.alfresco.jlan.server.filesys.FileName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This is an instance of a "locked delete shuffle" triggered by a create of a + * file matching a specified pattern. + * + *

First implemented for TextEdit from MacOS Lion + * + *

+ * Sequence of operations. + * a) Lock file created. Typically with an obscure name. + * b) Temp file created in temporary folder + * c) Target file deleted + * d) Temp file renamed to target file. + * e) Lock file deleted + *

+ * If this filter is active then this is what happens. + * a) Lock file created. Lock file created (X). + * b) Temp file created - in another folder. + * c) Existing file deleted. Scenario kicks in to rename rather than delete. + * d) New file moved into place (X to Y). Scenario kicks in + * 1) renames file from step c + * 2) copies content from temp file to target file + * 3) deletes temp file. + * e) Lock file deleted. + */ +public class ScenarioLockedDeleteShuffleInstance implements ScenarioInstance +{ + private static Log logger = LogFactory.getLog(ScenarioLockedDeleteShuffleInstance.class); + + enum InternalState + { + NONE, + LOCKED, // Lock file has been created and not deleted + DELETE_SUBSTITUTED, // Scenario has intervened and renamed rather than delete + MOVED + } + + InternalState internalState = InternalState.NONE; + + private Date startTime = new Date(); + + private String lockName; + + private Ranking ranking; + + + /** + * Timeout in ms. Default 30 seconds. + */ + private long timeout = 30000; + + private boolean isComplete; + + /** + * Keep track of deletes that we substitute with a rename + * could be more than one if scenarios overlap + * + * From, TempFileName + */ + private Map deletes = new HashMap(); + + /** + * Evaluate the next operation + * @param operation + */ + public Command evaluate(Operation operation) + { + + /** + * Anti-pattern for all states - delete the lock file + */ + if(lockName != null) + { + if(operation instanceof DeleteFileOperation) + { + DeleteFileOperation d = (DeleteFileOperation)operation; + if(d.getName().equals(lockName)) + { + logger.debug("Anti-pattern : Lock file deleted"); + isComplete = true; + return null; + } + } + } + + /** + * Anti-pattern : timeout + */ + Date now = new Date(); + if(now.getTime() > startTime.getTime() + getTimeout()) + { + if(logger.isDebugEnabled()) + { + logger.debug("Instance timed out"); + } + } + + switch (internalState) + { + case NONE: + // Looking for a create transition + if(operation instanceof CreateFileOperation) + { + CreateFileOperation c = (CreateFileOperation)operation; + this.lockName = c.getName(); + if(logger.isDebugEnabled()) + { + logger.debug("entering LOCKED state: " + lockName); + } + internalState = InternalState.LOCKED; + return null; + } + else + { + // anything else bomb out + if(logger.isDebugEnabled()) + { + logger.debug("State error, expected a CREATE"); + } + isComplete = true; + } + break; + + case LOCKED: + + /** + * Looking for target file being deleted + * + * Need to intervene and replace delete with a rename to temp file. + */ + if(operation instanceof DeleteFileOperation) + { + DeleteFileOperation d = (DeleteFileOperation)operation; + + + if(logger.isDebugEnabled()) + { + logger.debug("entering DELETE_SUBSTITUTED state: " + lockName); + } + + String tempName = ".shuffle" + d.getName(); + + deletes.put(d.getName(), tempName); + + String[] paths = FileName.splitPath(d.getPath()); + String currentFolder = paths[0]; + + RenameFileCommand r1 = new RenameFileCommand(d.getName(), tempName, d.getRootNodeRef(), d.getPath(), currentFolder + "\\" + tempName); + + internalState = InternalState.DELETE_SUBSTITUTED; + + return r1; + + } + + case DELETE_SUBSTITUTED: + + /** + * Looking for a move operation of the deleted file + */ + if(operation instanceof MoveFileOperation) + { + MoveFileOperation m = (MoveFileOperation)operation; + + String targetFile = m.getTo(); + + if(deletes.containsKey(targetFile)) + { + String tempName = deletes.get(targetFile); + + String[] paths = FileName.splitPath(m.getToPath()); + String currentFolder = paths[0]; + + /** + * This is where the scenario fires. + * a) Rename the temp file back to the targetFile + * b) Copy content from moved file + * c) Delete rather than move file + */ + ArrayList commands = new ArrayList(); + + RenameFileCommand r1 = new RenameFileCommand(tempName, targetFile, m.getRootNodeRef(), currentFolder + "\\" + tempName, m.getToPath()); + + CopyContentCommand copyContent = new CopyContentCommand(m.getFrom(), targetFile, m.getRootNodeRef(), m.getFromPath(), m.getToPath()); + + DeleteFileCommand d1 = new DeleteFileCommand(m.getFrom(), m.getRootNodeRef(), m.getFromPath()); + + commands.add(r1); + commands.add(copyContent); + commands.add(d1); + + logger.debug("Scenario complete"); + isComplete = true; + + return new CompoundCommand(commands); + + } + + //TODO - Need to consider error cases and "overlap" + +// if(logger.isDebugEnabled()) +// { +// logger.debug("entering MOVED state: " + lockName); +// } +// internalState = InternalState.MOVED; + } + + + case MOVED: + + } + + return null; + } + + @Override + public boolean isComplete() + { + return isComplete; + } + + @Override + public Ranking getRanking() + { + return ranking; + } + + public void setRanking(Ranking ranking) + { + this.ranking = ranking; + } + + public String toString() + { + return "ScenarioLockedDeleteShuffleInstance:" + lockName; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public long getTimeout() + { + return timeout; + } +} diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBufferedInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBufferedInstance.java index e5b43068af..7fcbc6cfe8 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBufferedInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioSimpleNonBufferedInstance.java @@ -71,7 +71,7 @@ public class ScenarioSimpleNonBufferedInstance implements ScenarioInstance else if(operation instanceof MoveFileOperation) { MoveFileOperation m = (MoveFileOperation)operation; - return new MoveFileCommand(m.getFrom(), m.getTo()); + return new MoveFileCommand(m.getFrom(), m.getTo(), m.getRootNodeRef(), m.getFromPath(), m.getToPath()); } else if(operation instanceof OpenFileOperation) { diff --git a/source/java/org/alfresco/filesys/repo/rules/commands/MoveFileCommand.java b/source/java/org/alfresco/filesys/repo/rules/commands/MoveFileCommand.java index 64ab21ff53..6d2d69dd99 100644 --- a/source/java/org/alfresco/filesys/repo/rules/commands/MoveFileCommand.java +++ b/source/java/org/alfresco/filesys/repo/rules/commands/MoveFileCommand.java @@ -22,20 +22,24 @@ import java.util.List; import org.alfresco.filesys.repo.rules.Command; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.service.cmr.repository.NodeRef; -/** - * Rename command - */ public class MoveFileCommand implements Command { private String from; private String to; + private NodeRef rootNode; + private String fromPath; + private String toPath; - public MoveFileCommand(String from, String to) + public MoveFileCommand(String from, String to, NodeRef rootNode, String fromPath, String toPath) { this.from = from; this.to = to; + this.rootNode = rootNode; + this.fromPath = fromPath; + this.toPath = toPath; } @@ -55,4 +59,40 @@ public class MoveFileCommand implements Command { return TxnReadState.TXN_READ_WRITE; } + + + public void setRootNode(NodeRef rootNode) + { + this.rootNode = rootNode; + } + + + public NodeRef getRootNode() + { + return rootNode; + } + + + public void setFromPath(String fromPath) + { + this.fromPath = fromPath; + } + + + public String getFromPath() + { + return fromPath; + } + + + public void setToPath(String toPath) + { + this.toPath = toPath; + } + + + public String getToPath() + { + return toPath; + } } diff --git a/source/java/org/alfresco/filesys/repo/rules/operations/DeleteFileOperation.java b/source/java/org/alfresco/filesys/repo/rules/operations/DeleteFileOperation.java index 5aa187fa98..65eadf26ae 100644 --- a/source/java/org/alfresco/filesys/repo/rules/operations/DeleteFileOperation.java +++ b/source/java/org/alfresco/filesys/repo/rules/operations/DeleteFileOperation.java @@ -28,6 +28,12 @@ public class DeleteFileOperation implements Operation private NodeRef rootNodeRef; private String path; + /** + * Delete File Operation + * @param name of file + * @param rootNodeRef root node ref + * @param path path + name of file to delete + */ public DeleteFileOperation(String name, NodeRef rootNodeRef, String path) { this.name = name; diff --git a/source/java/org/alfresco/filesys/repo/rules/operations/MoveFileOperation.java b/source/java/org/alfresco/filesys/repo/rules/operations/MoveFileOperation.java index 55da175e80..637daea827 100644 --- a/source/java/org/alfresco/filesys/repo/rules/operations/MoveFileOperation.java +++ b/source/java/org/alfresco/filesys/repo/rules/operations/MoveFileOperation.java @@ -19,16 +19,34 @@ package org.alfresco.filesys.repo.rules.operations; import org.alfresco.filesys.repo.rules.Operation; +import org.alfresco.service.cmr.repository.NodeRef; +/** + * Rename a file within the same directory + */ public class MoveFileOperation implements Operation { private String from; private String to; + private String fromPath; + private String toPath; + NodeRef rootNodeRef; - public MoveFileOperation(String from, String to) + /** + * + * @param from name of file from + * @param to name of file to + * @param fromPath full path of from + * @param toPath full path of to + * @param rootNodeRef + */ + public MoveFileOperation(String from, String to, String fromPath, String toPath, NodeRef rootNodeRef) { this.from = from; this.to = to; + this.fromPath = fromPath; + this.toPath = toPath; + this.rootNodeRef = rootNodeRef; } @@ -42,29 +60,44 @@ public class MoveFileOperation implements Operation return to; } + public String getToPath() + { + return toPath; + } + + public String getFromPath() + { + return fromPath; + } + + public NodeRef getRootNodeRef() + { + return rootNodeRef; + } + public String toString() { - return "MoveFileOperation: from " + from + " to "+ to; + return "MoveFileOperation: from " + fromPath + " to "+ toPath; } public int hashCode() { - return from.hashCode(); + return fromPath.hashCode(); } public boolean equals(Object o) { if(o instanceof MoveFileOperation) { - MoveFileOperation r = (MoveFileOperation)o; - if(from.equals(r.getFrom()) && to.equals(r.getTo())) + RenameFileOperation r = (RenameFileOperation)o; + if(fromPath.equals(r.getFromPath()) && toPath.equals(r.getToPath())) { return true; } } return false; } - - - } + + + diff --git a/source/java/org/alfresco/repo/avm/OrphanReaper.java b/source/java/org/alfresco/repo/avm/OrphanReaper.java index 7f6f45c2dc..725754c46a 100644 --- a/source/java/org/alfresco/repo/avm/OrphanReaper.java +++ b/source/java/org/alfresco/repo/avm/OrphanReaper.java @@ -20,11 +20,13 @@ package org.alfresco.repo.avm; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.alfresco.repo.domain.avm.AVMHistoryLinkEntity; import org.alfresco.repo.domain.avm.AVMMergeLinkEntity; import org.alfresco.repo.domain.permissions.Acl; import org.alfresco.repo.lock.JobLockService; +import org.alfresco.repo.lock.JobLockService.JobLockRefreshCallback; import org.alfresco.repo.lock.LockAcquisitionException; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.ContentData; @@ -46,7 +48,7 @@ public class OrphanReaper { synchronized (this) { - if (fRunning) + if (fRunning.get()) { if (fgLogger.isDebugEnabled()) { @@ -56,7 +58,7 @@ public class OrphanReaper return; } - fRunning = true; + fRunning.set(true); if (fgLogger.isTraceEnabled()) { @@ -68,7 +70,7 @@ public class OrphanReaper do { doBatch(); - if (fDone) + if (fDone.get()) { if (fgLogger.isTraceEnabled()) { @@ -90,13 +92,13 @@ public class OrphanReaper // Do nothing. } } - while (fActive); + while (fActive.get()); } finally { synchronized (this) { - fRunning = false; + fRunning.set(false); if (fgLogger.isTraceEnabled()) { @@ -116,33 +118,39 @@ public class OrphanReaper */ private TransactionService fTransactionService; + /** + * How many ms before refreshing the lock? + */ + private long lockRefreshTime = 60000; + + /** + * How long in ms to keep the lock in total before giving up, just in case there is a dead lock. + */ + private long lockTimeOut = 3600000; + /** * Active base sleep interval. */ - private long fActiveBaseSleep; + private long fActiveBaseSleep = 1000; /** * Batch size. */ - private int fBatchSize; + private int fBatchSize = 50; /** * Whether we are currently active, ie have work queued up. + * Using Atomics so that the memory model is synchronized between threads. */ - private boolean fActive; - - private boolean fDone = false; - - private boolean fRunning = false; - + private AtomicBoolean fActive = new AtomicBoolean(false); + private AtomicBoolean fDone = new AtomicBoolean(false); + private AtomicBoolean fRunning = new AtomicBoolean(false); + /** * Create one with default parameters. */ public OrphanReaper() { - fActiveBaseSleep = 1000; - fBatchSize = 50; - fActive = false; } // Setters for configuration. @@ -188,20 +196,37 @@ public class OrphanReaper this.jobLockService = jobLockService; } - /** - * Start things up after configuration is complete. - */ - // public void init() - // { - // fThread = new Thread(this); - // fThread.start(); - // } + public void setLockRefreshTime(long lockRefreshTime) + { + this.lockRefreshTime = lockRefreshTime; + } + + public long getLockRefreshTime() + { + return lockRefreshTime; + } + + public long getTimeToLive() + { + return getLockRefreshTime() * 2; + } + + public void setLockTimeOut(long lockTimeOut) + { + this.lockTimeOut = lockTimeOut; + } + + public long getLockTimeOut() + { + return lockTimeOut; + } + /** * Shutdown the reaper. This needs to be called when the application shuts down. */ public void shutDown() { - fDone = true; + fDone.set(true); } /** @@ -209,11 +234,11 @@ public class OrphanReaper * * @return Returns the lock token or null */ - private String getLock(long time) + private String getLock() { try { - return jobLockService.getLock(LOCK, time); + return jobLockService.getLock(LOCK, getTimeToLive()); } catch (LockAcquisitionException e) { @@ -222,48 +247,54 @@ public class OrphanReaper } /** - * Attempts to get the lock. If it fails, the current transaction is marked for rollback. - * - * @return Returns the lock token + * Creates a callback to refresh the lock if we are still doing work. + * @param lockToken to refresh + * @param lockHeld flag to indicate if the lock is needed/held. + * @param start when processing started. */ - private void refreshLock(String lockToken, long time) + private void createLockRefreshCallback(final String lockToken, final AtomicBoolean lockHeld, final long start) { if (lockToken == null) { throw new IllegalArgumentException("Must provide existing lockToken"); } - jobLockService.refreshLock(lockToken, LOCK, time); + + JobLockRefreshCallback callback = new JobLockRefreshCallback() + { + @Override + public boolean isActive() + { + boolean active = lockHeld.get(); + if (active) + { + // Check for deadlock + if (System.currentTimeMillis() >= start + getLockTimeOut()) + { + active = false; + lockHeld.set(false); // if not deadlocked this stop processing in the main thread + fgLogger.error("Lock held too long. Do we have a deadlock? Restart process."); + } + } + return active; + } + + @Override + public void lockReleased() + { + lockHeld.set(false); + } + }; + + jobLockService.refreshLock(lockToken, LOCK, getTimeToLive(), callback); } - - /** - * Sit in a loop, periodically querying for orphans. When orphans are found, unhook them in bite sized batches. - */ - // public void run() - // { - // while (!fDone) - // { - // synchronized (this) - // { - // try - // { - // wait(fActive? fActiveBaseSleep : fInactiveBaseSleep); - // } - // catch (InterruptedException ie) - // { - // // Do nothing. - // } - // doBatch(); - // } - // } - // } /** * This is really for debugging and testing. Allows another thread to mark the orphan reaper busy so that it can * monitor for it's being done. */ public void activate() { - fActive = true; + fActive.set(true); } /** @@ -273,7 +304,7 @@ public class OrphanReaper */ public boolean isActive() { - return fActive; + return fActive.get(); } /** @@ -285,20 +316,30 @@ public class OrphanReaper { public Object execute() throws Exception { - String lockToken = getLock(20000L); + final long start = System.currentTimeMillis(); + int reapCnt = 0; + + String lockToken = getLock(); if (lockToken == null) { fgLogger.info("Can't get lock. Assume multiple reapers ..."); - fActive = false; + fActive.set(false); return null; } - if (fgLogger.isTraceEnabled()) + AtomicBoolean lockHeld = new AtomicBoolean(true); + try { - fgLogger.trace("Orphan reaper doBatch: batchSize="+fBatchSize+", fActiveBaseSleep="+fActiveBaseSleep); - } + // Creates a callback that refreshes the lock as long the code in this try block is + // still running. If the JVM crashes, the lock will time out. Just in case the lock + // still times out, we check at several points in processing and have an overall + // timeout in case of deadlock. + createLockRefreshCallback(lockToken, lockHeld, start); + if (fgLogger.isTraceEnabled()) + { + fgLogger.trace("Orphan reaper doBatch: batchSize="+fBatchSize+", fActiveBaseSleep="+fActiveBaseSleep); + } - refreshLock(lockToken, fBatchSize * 100L); List nodes = AVMDAOs.Instance().fAVMNodeDAO.getOrphans(fBatchSize); if (nodes.size() == 0) { @@ -307,11 +348,14 @@ public class OrphanReaper fgLogger.trace("Nothing to purge (set fActive = false)"); } - fActive = false; + fActive.set(false); return null; } - refreshLock(lockToken, nodes.size() * 100L); + if (!lockHeld.get()) + { + throw new LockAcquisitionException("Lock lost. Finding orphans to reap."); + } LinkedList fPurgeQueue = new LinkedList(); for (AVMNode node : nodes) { @@ -323,125 +367,150 @@ public class OrphanReaper fgLogger.debug("Queue was empty so got more orphans from DB. Orphan queue size = "+fPurgeQueue.size()); } - fActive = true; + fActive.set(true); - int reapCnt = 0; - - long start = System.currentTimeMillis(); - - for (int i = 0; i < fBatchSize; i++) - { - if (fPurgeQueue.size() == 0) + for (int i = 0; i < fBatchSize; i++) { - if (fgLogger.isTraceEnabled()) + if (fPurgeQueue.size() == 0) { - fgLogger.trace("Purge queue is empty (fpurgeQueue size = "+fPurgeQueue.size()+")"); - } - - fPurgeQueue = null; - break; - } - - refreshLock(lockToken, 10000L); - Long nodeId = fPurgeQueue.removeFirst(); - AVMNode node = AVMDAOs.Instance().fAVMNodeDAO.getByID(nodeId); - - // Save away the ancestor and merged from fields from this node. - - AVMNode ancestor = null; - AVMHistoryLinkEntity hlEntity = AVMDAOs.Instance().newAVMNodeLinksDAO.getHistoryLinkByDescendent(node.getId()); - if (hlEntity != null) - { - ancestor = AVMDAOs.Instance().fAVMNodeDAO.getByID(hlEntity.getAncestorNodeId()); - AVMDAOs.Instance().newAVMNodeLinksDAO.deleteHistoryLink(hlEntity.getAncestorNodeId(), hlEntity.getDescendentNodeId()); - } - - AVMNode mergedFrom = null; - AVMMergeLinkEntity mlEntity = AVMDAOs.Instance().newAVMNodeLinksDAO.getMergeLinkByTo(node.getId()); - if (mlEntity != null) - { - mergedFrom = AVMDAOs.Instance().fAVMNodeDAO.getByID(mlEntity.getMergeFromNodeId()); - AVMDAOs.Instance().newAVMNodeLinksDAO.deleteMergeLink(mlEntity.getMergeFromNodeId(), mlEntity.getMergeToNodeId()); - } - - // Get all the nodes that have this node as ancestor. - List hlEntities = AVMDAOs.Instance().newAVMNodeLinksDAO.getHistoryLinksByAncestor(node.getId()); - for (AVMHistoryLinkEntity link : hlEntities) - { - AVMNode desc = AVMDAOs.Instance().fAVMNodeDAO.getByID(link.getDescendentNodeId()); - if (desc != null) - { - desc.setAncestor(ancestor); - if (desc.getMergedFrom() == null) + if (fgLogger.isTraceEnabled()) { - desc.setMergedFrom(mergedFrom); + fgLogger.trace("Purge queue is empty (fpurgeQueue size = " + + fPurgeQueue.size() + ")"); + } + + fPurgeQueue = null; + break; + } + + if (!lockHeld.get()) + { + throw new LockAcquisitionException("Lock lost. Orphan reap loop: "+i); + } + Long nodeId = fPurgeQueue.removeFirst(); + AVMNode node = AVMDAOs.Instance().fAVMNodeDAO.getByID(nodeId); + + // Save away the ancestor and merged from fields from + // this node. + + AVMNode ancestor = null; + AVMHistoryLinkEntity hlEntity = AVMDAOs.Instance().newAVMNodeLinksDAO + .getHistoryLinkByDescendent(node.getId()); + if (hlEntity != null) + { + ancestor = AVMDAOs.Instance().fAVMNodeDAO.getByID(hlEntity + .getAncestorNodeId()); + AVMDAOs.Instance().newAVMNodeLinksDAO.deleteHistoryLink( + hlEntity.getAncestorNodeId(), hlEntity.getDescendentNodeId()); + } + + AVMNode mergedFrom = null; + AVMMergeLinkEntity mlEntity = AVMDAOs.Instance().newAVMNodeLinksDAO + .getMergeLinkByTo(node.getId()); + if (mlEntity != null) + { + mergedFrom = AVMDAOs.Instance().fAVMNodeDAO.getByID(mlEntity + .getMergeFromNodeId()); + AVMDAOs.Instance().newAVMNodeLinksDAO.deleteMergeLink( + mlEntity.getMergeFromNodeId(), mlEntity.getMergeToNodeId()); + } + + // Get all the nodes that have this node as ancestor. + List hlEntities = AVMDAOs.Instance().newAVMNodeLinksDAO + .getHistoryLinksByAncestor(node.getId()); + for (AVMHistoryLinkEntity link : hlEntities) + { + AVMNode desc = AVMDAOs.Instance().fAVMNodeDAO.getByID(link + .getDescendentNodeId()); + if (desc != null) + { + desc.setAncestor(ancestor); + if (desc.getMergedFrom() == null) + { + desc.setMergedFrom(mergedFrom); + } + } + AVMDAOs.Instance().newAVMNodeLinksDAO.deleteHistoryLink( + link.getAncestorNodeId(), link.getDescendentNodeId()); + } + // Get all the nodes that have this node as mergedFrom + List mlEntities = AVMDAOs.Instance().newAVMNodeLinksDAO + .getMergeLinksByFrom(node.getId()); + for (AVMMergeLinkEntity link : mlEntities) + { + AVMNode mto = AVMDAOs.Instance().fAVMNodeDAO.getByID(link + .getMergeToNodeId()); + if (mto != null) + { + mto.setMergedFrom(ancestor); + } + AVMDAOs.Instance().newAVMNodeLinksDAO.deleteMergeLink( + link.getMergeFromNodeId(), link.getMergeToNodeId()); + } + + // Get rid of all properties belonging to this node. + AVMDAOs.Instance().fAVMNodeDAO.deleteProperties(node.getId()); + + // Get rid of all aspects belonging to this node. + AVMDAOs.Instance().fAVMNodeDAO.deleteAspects(node.getId()); + + // Get rid of ACL. + @SuppressWarnings("unused") + Acl acl = node.getAcl(); + node.setAcl(null); + // Unused acls will be garbage collected + // Many acls will be shared + // Extra work for directories. + if (node.getType() == AVMNodeType.PLAIN_DIRECTORY + || node.getType() == AVMNodeType.LAYERED_DIRECTORY) + { + // First get rid of all child entries for the node. + AVMDAOs.Instance().fChildEntryDAO.deleteByParent(node); + } + else if (node.getType() == AVMNodeType.PLAIN_FILE) + { + PlainFileNode file = (PlainFileNode) node; + if (file.isLegacyContentData()) + { + // We quickly convert the old ContentData to the + // new storage + ContentData contentData = file.getContentData(); + file.setContentData(contentData); + } + Long contentDataId = file.getContentDataId(); + if (contentDataId != null) + { + // The ContentDataDAO will take care of + // dereferencing and cleanup + AVMDAOs.Instance().contentDataDAO.deleteContentData(contentDataId); } } - AVMDAOs.Instance().newAVMNodeLinksDAO.deleteHistoryLink(link.getAncestorNodeId(), link.getDescendentNodeId()); - } - // Get all the nodes that have this node as mergedFrom - List mlEntities = AVMDAOs.Instance().newAVMNodeLinksDAO.getMergeLinksByFrom(node.getId()); - for (AVMMergeLinkEntity link : mlEntities) - { - AVMNode mto = AVMDAOs.Instance().fAVMNodeDAO.getByID(link.getMergeToNodeId()); - if (mto != null) - { - mto.setMergedFrom(ancestor); - } - AVMDAOs.Instance().newAVMNodeLinksDAO.deleteMergeLink(link.getMergeFromNodeId(), link.getMergeToNodeId()); - } - - // Get rid of all properties belonging to this node. - AVMDAOs.Instance().fAVMNodeDAO.deleteProperties(node.getId()); - - // Get rid of all aspects belonging to this node. - AVMDAOs.Instance().fAVMNodeDAO.deleteAspects(node.getId()); - - // Get rid of ACL. - @SuppressWarnings("unused") - Acl acl = node.getAcl(); - node.setAcl(null); - // Unused acls will be garbage collected - // Many acls will be shared - // Extra work for directories. - if (node.getType() == AVMNodeType.PLAIN_DIRECTORY || node.getType() == AVMNodeType.LAYERED_DIRECTORY) - { - // First get rid of all child entries for the node. - AVMDAOs.Instance().fChildEntryDAO.deleteByParent(node); - } - else if (node.getType() == AVMNodeType.PLAIN_FILE) - { - PlainFileNode file = (PlainFileNode)node; - if (file.isLegacyContentData()) - { - // We quickly convert the old ContentData to the new storage - ContentData contentData = file.getContentData(); - file.setContentData(contentData); - } - Long contentDataId = file.getContentDataId(); - if (contentDataId != null) - { - // The ContentDataDAO will take care of dereferencing and cleanup - AVMDAOs.Instance().contentDataDAO.deleteContentData(contentDataId); - } - } - + // Finally, delete it AVMDAOs.Instance().fAVMNodeDAO.delete(node); - + if (fgLogger.isTraceEnabled()) { - fgLogger.trace("Deleted Node ["+node.getId()+"]"); + fgLogger.trace("Deleted Node [" + node.getId() + "]"); } - - reapCnt++; + + reapCnt++; + } + // Check we still have the lock at the end + if (!lockHeld.get()) + { + throw new LockAcquisitionException("Lock lost at the end of processing"); + } } - - jobLockService.releaseLock(lockToken, LOCK); - - if (fgLogger.isDebugEnabled()) + finally { - fgLogger.debug("Reaped "+reapCnt+" nodes in "+(System.currentTimeMillis()-start)+" msecs"); + lockHeld.set(false); + jobLockService.releaseLock(lockToken, LOCK); + + if (fgLogger.isDebugEnabled()) + { + fgLogger.debug("Reaped "+reapCnt+" nodes in "+(System.currentTimeMillis()-start)+" ms"); + } } return null; @@ -456,7 +525,7 @@ public class OrphanReaper } catch (Exception e) { - fgLogger.error("Garbage collector error", e); + fgLogger.warn("Garbage collector error. Restarting process", e); } } } diff --git a/source/java/org/alfresco/repo/calendar/CalendarServiceImpl.java b/source/java/org/alfresco/repo/calendar/CalendarServiceImpl.java index e030b5059c..6295c87edc 100644 --- a/source/java/org/alfresco/repo/calendar/CalendarServiceImpl.java +++ b/source/java/org/alfresco/repo/calendar/CalendarServiceImpl.java @@ -294,7 +294,7 @@ public class CalendarServiceImpl implements CalendarService // Run the canned query GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_GET_CHILDREN); GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery( - container, null, types, null, sort, paging); + container, null, null, types, null, sort, paging); // Execute the canned query CannedQueryResults results = cq.execute(); diff --git a/source/java/org/alfresco/repo/content/ContentServiceImpl.java b/source/java/org/alfresco/repo/content/ContentServiceImpl.java index 29a1f1e5b4..34e7de94d3 100644 --- a/source/java/org/alfresco/repo/content/ContentServiceImpl.java +++ b/source/java/org/alfresco/repo/content/ContentServiceImpl.java @@ -562,6 +562,10 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa throws NoTransformerException, ContentIOException { // check that source and target mimetypes are available + if (reader == null) + { + throw new AlfrescoRuntimeException("The content reader must be set"); + } String sourceMimetype = reader.getMimetype(); if (sourceMimetype == null) { diff --git a/source/java/org/alfresco/repo/dictionary/TestModel.java b/source/java/org/alfresco/repo/dictionary/TestModel.java index 4f0c05a7cc..5eeab22062 100644 --- a/source/java/org/alfresco/repo/dictionary/TestModel.java +++ b/source/java/org/alfresco/repo/dictionary/TestModel.java @@ -36,13 +36,23 @@ import org.alfresco.repo.tenant.TenantService; */ public class TestModel { - + /** + * Test model + * + * Java command line client + *
+ * Syntax: + *
+ * TestModel [-h] [model filename]* + *

+ * Returns 0 for success. + */ 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.exit(1); } System.out.println("Testing dictionary model definitions..."); @@ -91,6 +101,9 @@ public class TestModel bootstrap.setDictionaryDAO(dictionaryDAO); bootstrap.bootstrap(); System.out.println("Models are valid."); + + System.exit(0); // Success + } catch(Exception e) { @@ -101,6 +114,7 @@ public class TestModel System.out.println(t.getMessage()); t = t.getCause(); } + System.exit(2); // Not Success } } diff --git a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java index b93e4405bf..7ac064e6e4 100644 --- a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -60,6 +60,7 @@ import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.TransactionAwareSingleton; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.InvalidTypeException; @@ -85,7 +86,7 @@ import org.alfresco.util.GUID; import org.alfresco.util.Pair; import org.alfresco.util.PropertyCheck; import org.alfresco.util.ReadWriteLockExecuter; -import org.alfresco.util.SerializationUtils; +import org.alfresco.util.ValueProtectingMap; import org.alfresco.util.EqualsHelper.MapValueComparison; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -992,9 +993,15 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO * @throws ConcurrencyFailureException if the ID doesn't reference a live node */ private Node getNodeNotNull(Long nodeId) + { + return getNodeNotNullImpl(nodeId, false); + } + + private Node getNodeNotNullImpl(Long nodeId, boolean deleted) { Pair pair = nodesCache.getByKey(nodeId); - if (pair == null || pair.getSecond().getDeleted()) + + if (pair == null || (pair.getSecond().getDeleted() && (!deleted))) { // Force a removal from the cache nodesCache.removeByKey(nodeId); @@ -1009,11 +1016,11 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO } else { - throw new ConcurrencyFailureException( - "No live node exists: \n" + + logger.warn("No live node exists: \n" + " ID: " + nodeId + "\n" + " Cache row: " + pair.getSecond() + "\n" + " DB row: " + dbNode); + throw new NotLiveNodeException(pair); } } else @@ -1110,7 +1117,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO childNodeName = node.getUuid(); } ChildAssocEntity assoc = newChildAssocImpl( - parentNodeId, nodeId, true, assocTypeQName, assocQName, childNodeName); + parentNodeId, nodeId, true, assocTypeQName, assocQName, childNodeName, false); // There will be no other parent assocs boolean isRoot = false; @@ -1821,6 +1828,8 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO public Map getNodeProperties(Long nodeId) { Map props = getNodePropertiesCached(nodeId); + // Create a shallow copy to allow additions + props = new HashMap(props); Node node = getNodeNotNull(nodeId); // Handle sys:referenceable @@ -1838,6 +1847,10 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO props.putAll(auditableProperties.getAuditableProperties()); } + // Wrap to ensure that we only clone values if the client attempts to modify + // the map or retrieve values that might, themselves, be mutable + props = new ValueProtectingMap(props, NodePropertyValue.IMMUTABLE_CLASSES); + // Done if (isDebugEnabled) { @@ -1874,6 +1887,10 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO else { Map props = getNodePropertiesCached(nodeId); + // Wrap to ensure that we only clone values if the client attempts to modify + // the map or retrieve values that might, themselves, be mutable + props = new ValueProtectingMap(props, NodePropertyValue.IMMUTABLE_CLASSES); + // The 'get' here will clone the value if it is mutable value = props.get(propertyQName); } // Done @@ -2118,8 +2135,9 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO Map propsToCache = null; if (isAddOnly) { + // Copy cache properties for additions + propsToCache = new HashMap(oldPropsCached); // Combine the old and new properties - propsToCache = oldPropsCached; propsToCache.putAll(propsToAdd); } else @@ -2194,10 +2212,13 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO { // Touch the node; all caches are fine touchNode(nodeId, null, null, false, false, false); - // Update cache + // Get cache props Map cachedProps = getNodePropertiesCached(nodeId); - cachedProps.keySet().removeAll(propertyQNames); - setNodePropertiesCached(nodeId, cachedProps); + // Remove deleted properties + Map props = new HashMap(cachedProps); + props.keySet().removeAll(propertyQNames); + // Update cache + setNodePropertiesCached(nodeId, props); } // Done return deleteCount > 0; @@ -2250,7 +2271,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO } /** - * @return Returns a writable copy of the cached property map + * @return Returns the read-only cached property map */ private Map getNodePropertiesCached(Long nodeId) { @@ -2261,11 +2282,9 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO invalidateNodeCaches(nodeId); throw new DataIntegrityViolationException("Invalid node ID: " + nodeId); } + // We have the properties from the cache Map cachedProperties = cacheEntry.getSecond(); - // Need to return a harmlessly mutable map - Map properties = copyPropertiesAgainstModification(cachedProperties); - // Done - return properties; + return cachedProperties; } /** @@ -2277,7 +2296,6 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO private void setNodePropertiesCached(Long nodeId, Map properties) { NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey(); - properties = copyPropertiesAgainstModification(properties); propertiesCache.setValue(nodeVersionKey, Collections.unmodifiableMap(properties)); } @@ -2293,26 +2311,6 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO } } - /** - * Shallow-copies to a new map except for maps and collections that are binary serialized - */ - private Map copyPropertiesAgainstModification(Map original) - { - // Copy the values, ensuring that any collections are copied as well - Map copy = new HashMap((int)(original.size() * 1.3)); - for (Map.Entry element : original.entrySet()) - { - QName key = element.getKey(); - Serializable value = element.getValue(); - if (value instanceof Collection || value instanceof Map) - { - value = (Serializable) SerializationUtils.deserialize(SerializationUtils.serialize(value)); - } - copy.put(key, value); - } - return copy; - } - /** * Callback to cache node properties. The DAO callback only does the simple {@link #findByKey(Long)}. * @@ -2808,7 +2806,8 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO boolean isPrimary, final QName assocTypeQName, QName assocQName, - final String childNodeName) + final String childNodeName, + boolean allowDeletedChild) { Assert.notNull(parentNodeId, "parentNodeId"); Assert.notNull(childNodeId, "childNodeId"); @@ -2818,7 +2817,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO // Get parent and child nodes. We need them later, so just get them now. final Node parentNode = getNodeNotNull(parentNodeId); - final Node childNode = getNodeNotNull(childNodeId); + final Node childNode = getNodeNotNullImpl(childNodeId, allowDeletedChild); final ChildAssocEntity assoc = new ChildAssocEntity(); // Parent node @@ -2905,7 +2904,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId); // Create it ChildAssocEntity assoc = newChildAssocImpl( - parentNodeId, childNodeId, false, assocTypeQName, assocQName, childNodeName); + parentNodeId, childNodeId, false, assocTypeQName, assocQName, childNodeName, false); Long assocId = assoc.getId(); // Touch the node; all caches are fine touchNode(childNodeId, null, null, false, false, false); @@ -3478,6 +3477,142 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO return paths; } + private void bindFixAssocAndCollectLostAndFound(final Pair lostNodePair, final String lostName, final ChildAssocEntity assoc) + { + AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter() + { + @Override + public void afterRollback() + { + if (transactionService.getAllowWrite()) + { + // New transaction + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + if (assoc == null) + { + // 'child' with missing parent assoc => collect lost+found orphan child + collectLostAndFoundNode(lostNodePair, lostName); + logger.error("ALF-13066: Orphan child node has been re-homed under lost_found: "+lostNodePair); + } + else + { + // 'child' with deleted parent assoc => delete invalid parent assoc and if primary then collect lost+found orphan child + deleteChildAssoc(assoc.getId()); + logger.error("ALF-12358: Deleted parent - removed child assoc: "+assoc.getId()); + + if (assoc.isPrimary()) + { + collectLostAndFoundNode(lostNodePair, lostName); + logger.error("ALF-12358: Orphan child node has been re-homed under lost_found: "+lostNodePair); + } + } + + return null; + } + }; + transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true); + } + } + }); + } + + private void collectLostAndFoundNode(Pair lostNodePair, String lostName) + { + Long childNodeId = lostNodePair.getFirst(); + NodeRef lostNodeRef = lostNodePair.getSecond(); + + Long newParentNodeId = getOrCreateLostAndFoundContainer(lostNodeRef.getStoreRef()).getId(); + + String assocName = lostName+"-"+System.currentTimeMillis(); + // Create new primary assoc (re-home the orphan node under lost_found) + ChildAssocEntity assoc = newChildAssocImpl(newParentNodeId, + childNodeId, + true, + ContentModel.ASSOC_CHILDREN, + QName.createQName(assocName), + assocName, + true); + + // Touch the node; all caches are fine + touchNode(childNodeId, null, null, false, false, false); + + // update cache + boolean isRoot = false; + boolean isStoreRoot = false; + ParentAssocsInfo parentAssocInfo = new ParentAssocsInfo(isRoot, isStoreRoot, assoc); + setParentAssocsCached(childNodeId, parentAssocInfo); + + /* + // Update ACLs for moved tree - note: actually a NOOP if oldParentAclId is null + Long newParentAclId = newParentNode.getAclId(); + Long oldParentAclId = null; // unknown + accessControlListDAO.updateInheritance(childNodeId, oldParentAclId, newParentAclId); + */ + } + + private Node getOrCreateLostAndFoundContainer(StoreRef storeRef) + { + Pair rootNodePair = getRootNode(storeRef); + Long rootParentNodeId = rootNodePair.getFirst(); + + final List> nodes = new ArrayList>(1); + NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() + { + public boolean handle( + Pair childAssocPair, + Pair parentNodePair, + Pair childNodePair + ) + { + nodes.add(childNodePair); + // More results + return true; + } + + @Override + public boolean preLoadNodes() + { + return false; + } + + @Override + public boolean orderResults() + { + return false; + } + + @Override + public void done() + { + } + }; + Set assocTypeQNames = new HashSet(1); + assocTypeQNames.add(ContentModel.ASSOC_LOST_AND_FOUND); + getChildAssocs(rootParentNodeId, assocTypeQNames, callback); + + Node lostFoundNode = null; + if (nodes.size() > 0) + { + lostFoundNode = getNodeNotNull(nodes.get(0).getFirst()); + + if (nodes.size() > 1) + { + logger.warn("More than one lost_found, using first: "+lostFoundNode.getNodeRef()); + } + } + else + { + lostFoundNode = newNode(rootParentNodeId, ContentModel.ASSOC_LOST_AND_FOUND, ContentModel.ASSOC_LOST_AND_FOUND, storeRef, null, ContentModel.TYPE_LOST_AND_FOUND, Locale.US, ContentModel.ASSOC_LOST_AND_FOUND.getLocalName(), null).getChildNode(); + + logger.info("Created lost_found: "+lostFoundNode.getNodeRef()); + } + + return lostFoundNode; + } + /** * Build the paths for a node * @@ -3516,9 +3651,9 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO Pair rootNodePair = getRootNode(currentStoreRef); currentRootNodePair = new Pair(currentStoreRef, rootNodePair.getSecond()); } - + // get the parent associations of the given node - ParentAssocsInfo parentAssocInfo = getParentAssocsCached(currentNodeId); + ParentAssocsInfo parentAssocInfo = getParentAssocsCached(currentNodeId); // note: currently may throw NotLiveNodeException // does the node have parents boolean hasParents = parentAssocInfo.getParentAssocs().size() > 0; @@ -3583,8 +3718,12 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO // Force a retry. The cached node was stale throw new DataIntegrityViolationException("Stale cache detected for Node #" + currentNodeId); } - // We have a corrupt repository - throw new RuntimeException("Node without parents does not have root aspect: " + currentNodeRef); + + // We have a corrupt repository - non-root node has a missing parent ?! + bindFixAssocAndCollectLostAndFound(currentNodePair, "nonRootNodeWithoutParents", null); + + // throw - error will be logged and then bound txn listener (afterRollback) will be called + throw new NonRootNodeWithoutParentsException(currentNodePair); } // walk up each parent association for (Map.Entry entry : parentAssocInfo.getParentAssocs().entrySet()) @@ -3631,10 +3770,25 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO " Prepending path parent: \n" + " Parent node: " + parentNodePair); } - + // push the assoc stack, recurse and pop assocIdStack.push(assocId); - prependPaths(parentNodePair, currentRootNodePair, path, completedPaths, assocIdStack, primaryOnly); + + try + { + prependPaths(parentNodePair, currentRootNodePair, path, completedPaths, assocIdStack, primaryOnly); + } + catch (final NotLiveNodeException re) + { + if (re.getNodePair().equals(parentNodePair)) + { + // We have a corrupt repository - deleted parent pointing to live child ?! + bindFixAssocAndCollectLostAndFound(currentNodePair, "childNodeWithDeletedParent", assoc); + } + // rethrow - this will cause error/rollback + throw re; + } + assocIdStack.pop(); } // done diff --git a/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java b/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java index 7f086c6d98..57cb084ac8 100644 --- a/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java +++ b/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java @@ -27,8 +27,10 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; import java.util.Map; +import java.util.Set; import javax.crypto.SealedObject; @@ -45,6 +47,7 @@ import org.alfresco.service.cmr.repository.Period; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.namespace.QName; import org.alfresco.util.EqualsHelper; +import org.alfresco.util.ValueProtectingMap; import org.alfresco.util.VersionNumber; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -67,6 +70,31 @@ public class NodePropertyValue implements Cloneable, Serializable private static Log logger = LogFactory.getLog(NodePropertyValue.class); private static Log loggerOracle = LogFactory.getLog(NodePropertyValue.class.getName() + ".oracle"); + + /** + * Immutable classes in addition to {@link ValueProtectingMap#DEFAULT_IMMUTABLE_CLASSES} + *

  • ContentData
  • + *
  • ContentDataId
  • + *
  • NodeRef
  • + *
  • ChildAssociationRef
  • + *
  • AssociationRef
  • + *
  • QName
  • + *
  • VersionNumber
  • + *
  • Period
  • + */ + public static final Set> IMMUTABLE_CLASSES; + static + { + IMMUTABLE_CLASSES = new HashSet>(13); + IMMUTABLE_CLASSES.add(ContentData.class); + IMMUTABLE_CLASSES.add(ContentDataId.class); + IMMUTABLE_CLASSES.add(NodeRef.class); + IMMUTABLE_CLASSES.add(ChildAssociationRef.class); + IMMUTABLE_CLASSES.add(AssociationRef.class); + IMMUTABLE_CLASSES.add(QName.class); + IMMUTABLE_CLASSES.add(VersionNumber.class); + IMMUTABLE_CLASSES.add(Period.class); + } /** potential value types */ private static enum ValueType diff --git a/source/java/org/alfresco/repo/domain/node/NonRootNodeWithoutParentsException.java b/source/java/org/alfresco/repo/domain/node/NonRootNodeWithoutParentsException.java new file mode 100644 index 0000000000..f48d0924d0 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/node/NonRootNodeWithoutParentsException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.domain.node; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.Pair; +import org.springframework.dao.ConcurrencyFailureException; + +/** + * For internal use only: see ALF-13066 / ALF-12358 + */ +/* package */ class NonRootNodeWithoutParentsException extends ConcurrencyFailureException +{ + private static final long serialVersionUID = 5920138218201628243L; + + private final Pair nodePair; + + public NonRootNodeWithoutParentsException(Pair nodePair) + { + super("Node without parents does not have root aspect: " + nodePair); + this.nodePair = nodePair; + } + + public Pair getNodePair() + { + return nodePair; + } +} diff --git a/source/java/org/alfresco/repo/domain/node/NotLiveNodeException.java b/source/java/org/alfresco/repo/domain/node/NotLiveNodeException.java new file mode 100644 index 0000000000..a2825bb639 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/node/NotLiveNodeException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.domain.node; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.Pair; +import org.springframework.dao.ConcurrencyFailureException; + +/** + * For internal use only: see ALF-13066 / ALF-12358 + */ +/* package */ class NotLiveNodeException extends ConcurrencyFailureException +{ + private static final long serialVersionUID = 5920138218201628243L; + + private final Pair nodePair; + + public NotLiveNodeException(Pair nodePair) + { + super("Unexpected deleted node"); + this.nodePair = nodePair; + } + + public Pair getNodePair() + { + return new Pair(nodePair.getFirst(), nodePair.getSecond().getNodeRef()); + } +} diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index 4066e368c4..bf00960a82 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -966,11 +966,28 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol private void setFlag(NodeRef nodeRef, Flag flag, boolean value) { checkForFlaggableAspect(nodeRef); - AccessStatus status = permissionService.hasPermission(nodeRef, PermissionService.WRITE_PROPERTIES); + + String permission = (flag == Flag.DELETED ? PermissionService.DELETE_NODE : PermissionService.WRITE_PROPERTIES); + + AccessStatus status = permissionService.hasPermission(nodeRef, permission); if (status == AccessStatus.DENIED) { - logger.debug("[setFlag] Access denied to add FLAG to " + nodeRef); - //TODO should we throw an exception here? + if(flag == Flag.DELETED) + { + logger.debug("[setFlag] Access denied to set DELETED FLAG:" + nodeRef); + throw new AccessDeniedException("No permission to set DELETED flag"); + } + if(flag == Flag.SEEN) + { + logger.debug("[setFlag] Access denied to set SEEN FLAG:" + nodeRef); + //TODO - should we throw an exception here? + //throw new AccessDeniedException("No permission to set DELETED flag"); + } + else + { + logger.debug("[setFlag] Access denied to set flag:" + nodeRef); + throw new AccessDeniedException("No permission to set flag:" + flag.toString()); + } } else { diff --git a/source/java/org/alfresco/repo/jscript/ScriptNode.java b/source/java/org/alfresco/repo/jscript/ScriptNode.java index c5d3a9c830..10b468503f 100644 --- a/source/java/org/alfresco/repo/jscript/ScriptNode.java +++ b/source/java/org/alfresco/repo/jscript/ScriptNode.java @@ -2733,6 +2733,11 @@ public class ScriptNode implements Scopeable, NamespacePrefixResolverProvider String nodeMimeType = getMimetype(); Serializable value = this.nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, value); + if (contentData == null) + { + logger.info("Unable to create thumbnail '" + details.getName() + "' as there is no content"); + return null; + } if (!registry.isThumbnailDefinitionAvailable(contentData.getContentUrl(), nodeMimeType, getSize(), details)) { logger.info("Unable to create thumbnail '" + details.getName() + "' for " + diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java index 3a5b73bbec..6446add82c 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -486,9 +486,9 @@ public class FileFolderServiceImpl implements FileFolderService // get canned query GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_FILEFOLDER_LIST); - - GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(contextNodeRef, pattern, searchTypeQNames, null, sortProps, pagingRequest); - + + GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(contextNodeRef, pattern, Collections.singleton(ContentModel.ASSOC_CONTAINS), searchTypeQNames, null, sortProps, pagingRequest); + // execute canned query CannedQueryResults results = cq.execute(); diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java index 13bfe8c094..a75047da18 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java @@ -38,6 +38,7 @@ import org.alfresco.model.ForumModel; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.DictionaryBootstrap; import org.alfresco.repo.dictionary.DictionaryDAO; import org.alfresco.repo.dictionary.M2Model; import org.alfresco.repo.dictionary.M2Type; @@ -46,6 +47,7 @@ import org.alfresco.repo.node.integrity.IntegrityChecker; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileExistsException; import org.alfresco.service.cmr.model.FileFolderService; @@ -100,11 +102,13 @@ public class FileFolderServiceImplTest extends TestCase private NodeService nodeService; private FileFolderService fileFolderService; private PermissionService permissionService; + private TenantService tenantService; private MutableAuthenticationService authenticationService; private DictionaryDAO dictionaryDAO; private UserTransaction txn; private NodeRef rootNodeRef; private NodeRef workingRootNodeRef; + private NodeRef workingRootNodeRef1; @Override public void setUp() throws Exception @@ -116,6 +120,7 @@ public class FileFolderServiceImplTest extends TestCase permissionService = serviceRegistry.getPermissionService(); authenticationService = (MutableAuthenticationService) ctx.getBean("AuthenticationService"); dictionaryDAO = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + tenantService = (TenantService) ctx.getBean("tenantService"); // start the transaction txn = transactionService.getUserTransaction(); @@ -149,6 +154,33 @@ public class FileFolderServiceImplTest extends TestCase } Reader reader = new InputStreamReader(is); importerService.importView(reader, importLocation, null, null); + + // Load test model + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + List bootstrapModels = new ArrayList(); + bootstrapModels.add("org/alfresco/repo/model/filefolder/testModel.xml"); + List labels = new ArrayList(); + bootstrap.setModels(bootstrapModels); + bootstrap.setLabels(labels); + bootstrap.setDictionaryDAO(dictionaryDAO); + bootstrap.setTenantService(tenantService); + bootstrap.bootstrap(); + + workingRootNodeRef1 = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.ALFRESCO_URI, "working root1"), + QName.createQName("http://www.alfresco.org/test/filefoldertest/1.0", "folder")).getChildRef(); + nodeService.createNode( + workingRootNodeRef1, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.ALFRESCO_URI, "node1"), + ContentModel.TYPE_CONTENT).getChildRef(); + nodeService.createNode( + workingRootNodeRef1, + QName.createQName("http://www.alfresco.org/test/filefoldertest/1.0", "contains1"), + QName.createQName(NamespaceService.ALFRESCO_URI, "node2"), + ContentModel.TYPE_CONTENT).getChildRef(); } public void tearDown() throws Exception @@ -1327,4 +1359,14 @@ public class FileFolderServiceImplTest extends TestCase checkFileList(files, 2, 0, expectedNames); } + + public void testALF12758() + { + // test that the FileFolderService returns only cm:contains children + PagingRequest pagingRequest = new PagingRequest(0, Integer.MAX_VALUE); + PagingResults pagingResults = fileFolderService.list(workingRootNodeRef1, true, true, null, null, null, pagingRequest); + assertNotNull(pagingResults); + assertNotNull(pagingResults.getPage()); + assertEquals(1, pagingResults.getPage().size()); + } } diff --git a/source/java/org/alfresco/repo/model/filefolder/testModel.xml b/source/java/org/alfresco/repo/model/filefolder/testModel.xml new file mode 100644 index 0000000000..b0f63594b7 --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/testModel.xml @@ -0,0 +1,51 @@ + + + Alfresco Content Model + Alfresco + 20012-02-23 + 1.0 + + + + + + + + + + + + + + + + + + + + + Test Folder + cm:folder + true + + + + false + true + + + sys:base + false + true + + false + true + + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/NodeServiceTest.java b/source/java/org/alfresco/repo/node/NodeServiceTest.java index 8b57bb6255..6828ffd175 100644 --- a/source/java/org/alfresco/repo/node/NodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/NodeServiceTest.java @@ -22,12 +22,15 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; 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.Locale; import java.util.Map; +import java.util.Random; import java.util.Set; import junit.framework.TestCase; @@ -35,8 +38,11 @@ import junit.framework.TestCase; import org.alfresco.model.ContentModel; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.domain.node.Node; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.domain.node.NodeEntity; import org.alfresco.repo.domain.node.NodeVersionKey; import org.alfresco.repo.domain.node.ParentAssocsInfo; +import org.alfresco.repo.domain.query.CannedQueryDAO; import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeSetNodeTypePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeUpdateNodePolicy; @@ -66,7 +72,10 @@ 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.Pair; import org.alfresco.util.PropertyMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.extensions.surf.util.I18NUtil; @@ -86,10 +95,13 @@ public class NodeServiceTest extends TestCase private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private static Log logger = LogFactory.getLog(NodeServiceTest.class); + protected ServiceRegistry serviceRegistry; protected NodeService nodeService; private TransactionService txnService; private PolicyComponent policyComponent; + private CannedQueryDAO cannedQueryDAO; private SimpleCache nodesCache; private SimpleCache propsCache; private SimpleCache aspectsCache; @@ -108,6 +120,7 @@ public class NodeServiceTest extends TestCase nodeService = serviceRegistry.getNodeService(); txnService = serviceRegistry.getTransactionService(); policyComponent = (PolicyComponent) ctx.getBean("policyComponent"); + cannedQueryDAO = (CannedQueryDAO) ctx.getBean("cannedQueryDAO"); // Get the caches for later testing nodesCache = (SimpleCache) ctx.getBean("node.nodesSharedCache"); @@ -306,11 +319,13 @@ public class NodeServiceTest extends TestCase /** * Tests that two separate node trees can be deleted concurrently at the database level. - * This is not a concurren thread issue; instead we delete a hierarchy and hold the txn + * This is not a concurrent thread issue; instead we delete a hierarchy and hold the txn * open while we delete another in a new txn, thereby testing that DB locks don't prevent * concurrent deletes. *

    * See: ALF-5714 + * + * Note: if this test hangs for MySQL then check if 'innodb_locks_unsafe_for_binlog = true' (and restart MySQL + test) */ public void testConcurrentArchive() throws Exception { @@ -1039,4 +1054,296 @@ public class NodeServiceTest extends TestCase new JavaBehaviour(policy, policyQName.getLocalName())); return policy; } + + /** + * Ensure that nodes cannot be linked to deleted nodes. + *

    + * Conditions that might cause this are:
    + *

      + *
    • Node created within a parent node that is being deleted
    • + *
    • The node cache is temporarily incorrect when the association is made
    • + *
    + *

    + * Concurrency: Possible to create association references to deleted nodes + */ + public void testConcurrentLinkToDeletedNode() throws Throwable + { + // First find any broken links to start with + final NodeEntity params = new NodeEntity(); + params.setId(0L); + params.setDeleted(true); + + List ids = getChildNodesWithDeletedParentNode(params, 0); + logger.debug("Found child nodes with deleted parent node (before): " + ids); + + final int idsToSkip = ids.size(); + + final NodeRef[] nodeRefs = new NodeRef[10]; + final NodeRef workspaceRootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + buildNodeHierarchy(workspaceRootNodeRef, nodeRefs); + + // Fire off a bunch of threads that create random nodes within the hierarchy created above + final RetryingTransactionCallback createChildCallback = new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + String randomName = getName() + "-" + System.nanoTime(); + QName randomQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, randomName); + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, randomName); + // Choose a random parent node from the hierarchy + int random = new Random().nextInt(10); + return nodeService.createNode( + nodeRefs[random], + ContentModel.ASSOC_CONTAINS, + randomQName, + ContentModel.TYPE_CONTAINER, + props).getChildRef(); + } + }; + final Runnable[] runnables = new Runnable[20]; + final List nodesAtRisk = Collections.synchronizedList(new ArrayList(100)); + + final List threads = new ArrayList(); + for (int i = 0; i < runnables.length; i++) + { + runnables[i] = new Runnable() + { + @Override + public synchronized void run() + { + AuthenticationUtil.setRunAsUserSystem(); + try + { + wait(1000L); // A short wait before we kick off (should be notified) + for (int i = 0; i < 200; i++) + { + NodeRef nodeRef = txnService.getRetryingTransactionHelper().doInTransaction(createChildCallback); + // Store the node for later checks + nodesAtRisk.add(nodeRef); + // Wait to give other threads a chance + wait(1L); + } + } + catch (Throwable e) + { + // This is expected i.e. we'll just keep doing it until failure + logger.debug("Got exception adding child node: ", e); + } + } + }; + Thread thread = new Thread(runnables[i]); + threads.add(thread); + thread.start(); + } + + final RetryingTransactionCallback deleteWithNestedCallback = new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // Notify the threads to kick off + for (int i = 0; i < runnables.length; i++) + { + // Notify the threads to stop waiting + synchronized(runnables[i]) + { + runnables[i].notify(); + } + // Short wait to give thread a chance to run + synchronized(this) { try { wait(10L); } catch (Throwable e) {} }; + } + // Delete the parent node + nodeService.deleteNode(nodeRefs[0]); + return null; + } + }; + txnService.getRetryingTransactionHelper().doInTransaction(deleteWithNestedCallback); + + // Wait for the threads to finish + for (Thread t : threads) + { + t.join(); + } + + logger.info("All threads should have finished"); + + // Now need to identify the problem nodes + final List childNodeIds = getChildNodesWithDeletedParentNode(params, idsToSkip); + + if (childNodeIds.isEmpty()) + { + // nothing more to test + return; + } + + logger.debug("Found child nodes with deleted parent node (after): " + childNodeIds); + + // workaround recovery: force collection of any orphan nodes (ALF-12358 + ALF-13066) + for (NodeRef nodeRef : nodesAtRisk) + { + if (nodeService.exists(nodeRef)) + { + nodeService.getPath(nodeRef); // ignore return + } + } + + // check again ... + ids = getChildNodesWithDeletedParentNode(params, idsToSkip); + assertTrue("The following child nodes have deleted parent node: " + ids, ids.isEmpty()); + + // check lost_found ... + List lostAndFoundNodeRefs = getLostAndFoundNodes(); + assertFalse(lostAndFoundNodeRefs.isEmpty()); + + List lostAndFoundNodeIds = new ArrayList(lostAndFoundNodeRefs.size()); + for (NodeRef nodeRef : lostAndFoundNodeRefs) + { + lostAndFoundNodeIds.add((Long)nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_DBID)); + } + + for (Long childNodeId : childNodeIds) + { + assertTrue("Not found: "+childNodeId, lostAndFoundNodeIds.contains(childNodeId)); + } + } + + /** + * Pending repeatable test - force issue ALF-ALF-13066 (non-root node with no parent) + */ + public void testForceNonRootNodeWithNoParentNode() throws Throwable + { + final NodeEntity params = new NodeEntity(); + params.setId(0L); + params.setDeleted(true); + + List ids = getChildNodesWithNoParentNode(params, 0); + logger.debug("Found child nodes with deleted parent node (before): " + ids); + + final int idsToSkip = ids.size(); + + final NodeRef[] nodeRefs = new NodeRef[10]; + final NodeRef workspaceRootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + buildNodeHierarchy(workspaceRootNodeRef, nodeRefs); + + int cnt = 5; + List childNodeRefs = new ArrayList(cnt); + + final NodeDAO nodeDAO = (NodeDAO)ctx.getBean("nodeDAO"); + + for (int i = 0; i < cnt; i++) + { + // create some pseudo- thumnails + String randomName = getName() + "-" + System.nanoTime(); + QName randomQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, randomName); + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, randomName); + + // Choose a random parent node from the hierarchy + int random = new Random().nextInt(10); + NodeRef parentNodeRef = nodeRefs[random]; + + NodeRef childNodeRef = nodeService.createNode( + parentNodeRef, + ContentModel.ASSOC_CONTAINS, + randomQName, + ContentModel.TYPE_THUMBNAIL, + props).getChildRef(); + + childNodeRefs.add(childNodeRef); + + // forcefully remove the primary parent assoc + final Long childNodeId = (Long)nodeService.getProperty(childNodeRef, ContentModel.PROP_NODE_DBID); + txnService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + Pair assocPair = nodeDAO.getPrimaryParentAssoc(childNodeId); + nodeDAO.deleteChildAssoc(assocPair.getFirst()); + return null; + } + }); + } + + // Now need to identify the problem nodes + final List childNodeIds = getChildNodesWithNoParentNode(params, idsToSkip); + assertFalse(childNodeIds.isEmpty()); + logger.debug("Found child nodes with deleted parent node (after): " + childNodeIds); + + // workaround recovery: force collection of any orphan nodes (ALF-12358 + ALF-13066) + for (NodeRef nodeRef : childNodeRefs) + { + if (nodeService.exists(nodeRef)) + { + nodeService.getPath(nodeRef); // ignore return + } + } + + // check again ... + ids = getChildNodesWithNoParentNode(params, idsToSkip); + assertTrue("The following child nodes have no parent node: " + ids, ids.isEmpty()); + + // check lost_found ... + List lostAndFoundNodeRefs = getLostAndFoundNodes(); + assertFalse(lostAndFoundNodeRefs.isEmpty()); + + List lostAndFoundNodeIds = new ArrayList(lostAndFoundNodeRefs.size()); + for (NodeRef nodeRef : lostAndFoundNodeRefs) + { + lostAndFoundNodeIds.add((Long)nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_DBID)); + } + + for (Long childNodeId : childNodeIds) + { + assertTrue("Not found: "+childNodeId, lostAndFoundNodeIds.contains(childNodeId)); + } + } + + private List getChildNodesWithDeletedParentNode(NodeEntity params, int idsToSkip) + { + return cannedQueryDAO.executeQuery( + "alfresco.query.test", + "select_NodeServiceTest_testConcurrentLinkToDeletedNode_GetChildNodesWithDeletedParentNodeCannedQuery", + params, + idsToSkip, + Integer.MAX_VALUE); + } + + private List getChildNodesWithNoParentNode(NodeEntity params, int idsToSkip) + { + return cannedQueryDAO.executeQuery( + "alfresco.query.test", + "select_NodeServiceTest_testForceNonRootNodeWithNoParentNode_GetChildNodesWithNoParentNodeCannedQuery", + params, + idsToSkip, + Integer.MAX_VALUE); + } + + private List getLostAndFoundNodes() + { + Set childNodeTypeQNames = new HashSet(1); + childNodeTypeQNames.add(ContentModel.TYPE_LOST_AND_FOUND); + + List childAssocRefs = nodeService.getChildAssocs(nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE), childNodeTypeQNames); + + List lostNodeRefs = null; + + if (childAssocRefs.size() > 0) + { + List lostNodeChildAssocRefs = nodeService.getChildAssocs(childAssocRefs.get(0).getChildRef()); + lostNodeRefs = new ArrayList(lostNodeChildAssocRefs.size()); + for(ChildAssociationRef lostNodeChildAssocRef : lostNodeChildAssocRefs) + { + lostNodeRefs.add(lostNodeChildAssocRef.getChildRef()); + } + } + else + { + lostNodeRefs = Collections.emptyList(); + } + + return lostNodeRefs; + } } diff --git a/source/java/org/alfresco/repo/node/getchildren/FilterSortNodeEntity.java b/source/java/org/alfresco/repo/node/getchildren/FilterSortNodeEntity.java index f535b111ab..dad56b5798 100644 --- a/source/java/org/alfresco/repo/node/getchildren/FilterSortNodeEntity.java +++ b/source/java/org/alfresco/repo/node/getchildren/FilterSortNodeEntity.java @@ -19,6 +19,7 @@ package org.alfresco.repo.node.getchildren; import java.util.List; +import java.util.Set; import org.alfresco.repo.domain.node.NodeEntity; import org.alfresco.repo.domain.node.NodePropertyEntity; @@ -46,6 +47,7 @@ public class FilterSortNodeEntity private Long prop2qnameId; private Long prop3qnameId; private List childNodeTypeQNameIds; + private Set assocTypeQNameIds; private String pattern; private Long namePropertyQNameId; private boolean auditableProps; @@ -106,6 +108,16 @@ public class FilterSortNodeEntity } } + public void setAssocTypeQNameIds(Set assocTypeQNameIds) + { + this.assocTypeQNameIds = assocTypeQNameIds; + } + + public Set getAssocTypeQNameIds() + { + return assocTypeQNameIds; + } + public Long getNamePropertyQNameId() { return namePropertyQNameId; diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java index fe31d14c81..5f1bc98a5d 100644 --- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java +++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java @@ -151,6 +151,7 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions childNodeTypeQNames = paramBean.getChildTypeQNames(); + Set assocTypeQNames = paramBean.getAssocTypeQNames(); final List filterProps = paramBean.getFilterProps(); String pattern = paramBean.getPattern(); @@ -158,7 +159,7 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions> sortPairs = (List)sortDetails.getSortPairs(); - + // Set sort / filter params // Note - need to keep the sort properties in their requested order List sortFilterProps = new ArrayList(filterProps.size() + sortPairs.size()); @@ -199,6 +200,15 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions assocTypeQNameIds = qnameDAO.convertQNamesToIds(assocTypeQNames, false); + if (assocTypeQNameIds.size() > 0) + { + params.setAssocTypeQNameIds(assocTypeQNameIds); + } + } + if (pattern != null) { // TODO, check that we should be tied to the content model in this way. Perhaps a configurable property diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java index fc11f8ed4c..09c51ec5ef 100644 --- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java +++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java @@ -117,6 +117,7 @@ public class GetChildrenCannedQueryFactory extends AbstractCannedQueryFactory getCannedQuery(NodeRef parentRef, String pattern, Set childTypeQNames, List filterProps, List> sortProps, PagingRequest pagingRequest) + public CannedQuery getCannedQuery(NodeRef parentRef, String pattern, Set assocTypeQNames, Set childTypeQNames, List filterProps, List> sortProps, PagingRequest pagingRequest) { ParameterCheck.mandatory("parentRef", parentRef); ParameterCheck.mandatory("pagingRequest", pagingRequest); @@ -132,7 +133,7 @@ public class GetChildrenCannedQueryFactory extends AbstractCannedQueryFactory getCannedQuery(NodeRef parentRef, String pattern,Set childTypeQNames, PagingRequest pagingRequest) + public CannedQuery getCannedQuery(NodeRef parentRef, String pattern, Set assocTypeQNames, Set childTypeQNames, PagingRequest pagingRequest) { - return getCannedQuery(parentRef, pattern, childTypeQNames, null, null, pagingRequest); + return getCannedQuery(parentRef, pattern, assocTypeQNames, childTypeQNames, null, null, pagingRequest); } @Override diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryParams.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryParams.java index 4d47520f01..81243c9748 100644 --- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryParams.java +++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryParams.java @@ -37,15 +37,18 @@ public class GetChildrenCannedQueryParams private Set childTypeQNames = Collections.emptySet(); private List filterProps = Collections.emptyList(); + private Set assocTypeQNames = null; private String pattern = null; public GetChildrenCannedQueryParams( NodeRef parentRef, + Set assocTypeQNames, Set childTypeQNames, List filterProps, String pattern) { this.parentRef = parentRef; + this.assocTypeQNames = assocTypeQNames; if (childTypeQNames != null) { this.childTypeQNames = childTypeQNames; } if (filterProps != null) { this.filterProps = filterProps; } @@ -65,7 +68,12 @@ public class GetChildrenCannedQueryParams return childTypeQNames; } - public List getFilterProps() + public Set getAssocTypeQNames() + { + return assocTypeQNames; + } + + public List getFilterProps() { return filterProps; } diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java index de770f13a1..87c7072826 100644 --- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java +++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java @@ -40,6 +40,8 @@ import org.alfresco.query.CannedQueryFactory; import org.alfresco.query.CannedQueryResults; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; +import org.alfresco.repo.dictionary.DictionaryBootstrap; +import org.alfresco.repo.dictionary.DictionaryDAO; import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.domain.locale.LocaleDAO; import org.alfresco.repo.domain.node.NodeDAO; @@ -94,6 +96,8 @@ public class GetChildrenCannedQueryTest extends TestCase private MutableAuthenticationService authenticationService; private PermissionService permissionService; private RatingService ratingService; + private TenantService tenantService; + private DictionaryDAO dictionaryDAO; private RatingScheme fiveStarRatingScheme; private RatingScheme likesRatingScheme; @@ -113,6 +117,8 @@ public class GetChildrenCannedQueryTest extends TestCase private Set permHits = new HashSet(100); private Set permMisses = new HashSet(100); + private NodeRef testFolder; + @SuppressWarnings({ "rawtypes" }) private NamedObjectRegistry cannedQueryRegistry; @@ -130,6 +136,9 @@ public class GetChildrenCannedQueryTest extends TestCase authenticationService = (MutableAuthenticationService)ctx.getBean("AuthenticationService"); permissionService = (PermissionService)ctx.getBean("PermissionService"); ratingService = (RatingService)ctx.getBean("RatingService"); + + dictionaryDAO = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + tenantService = (TenantService) ctx.getBean("tenantService"); cannedQueryRegistry = new NamedObjectRegistry(); cannedQueryRegistry.setStorageType(CannedQueryFactory.class); @@ -160,6 +169,17 @@ public class GetChildrenCannedQueryTest extends TestCase { AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + // Load test model + DictionaryBootstrap bootstrap = new DictionaryBootstrap(); + List bootstrapModels = new ArrayList(); + bootstrapModels.add("org/alfresco/repo/node/getchildren/testModel.xml"); + List labels = new ArrayList(); + bootstrap.setModels(bootstrapModels); + bootstrap.setLabels(labels); + bootstrap.setDictionaryDAO(dictionaryDAO); + bootstrap.setTenantService(tenantService); + bootstrap.bootstrap(); + createUser(TEST_USER_PREFIX, TEST_USER, TEST_USER); createUser(TEST_USER_PREFIX+"aaaa", TEST_USER_PREFIX+"bbbb", TEST_USER_PREFIX+"cccc"); @@ -221,7 +241,12 @@ public class GetChildrenCannedQueryTest extends TestCase assertTrue(permissionService.hasPermission(nodeRef, PermissionService.READ) == AccessStatus.ALLOWED); } } - + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + testFolder = createFolder(repositoryHelper.getCompanyHome(), "testFolder1", QName.createQName("http://www.alfresco.org/test/getchildrentest/1.0", "folder")); + createContent(testFolder, "textContent1", ContentModel.TYPE_CONTENT); + createContent(testFolder, QName.createQName("http://www.alfresco.org/test/getchildrentest/1.0", "contains1"), "textContent2", ContentModel.TYPE_CONTENT); + AuthenticationUtil.setFullyAuthenticatedUser(TEST_USER); } @@ -689,6 +714,34 @@ public class GetChildrenCannedQueryTest extends TestCase assertEquals("", 0, totalCnt); } + public void testRestrictByAssocType() + { + Set assocTypeQNames = new HashSet(3); + Set childTypeQNames = new HashSet(3); + + assocTypeQNames.clear(); + assocTypeQNames.add(ContentModel.ASSOC_CONTAINS); + childTypeQNames.clear(); + childTypeQNames.add(ContentModel.TYPE_CONTENT); + List children = filterByAssocTypeAndCheck(testFolder, assocTypeQNames, childTypeQNames); + assertEquals(1, children.size()); + + assocTypeQNames.clear(); + assocTypeQNames.add(QName.createQName("http://www.alfresco.org/test/getchildrentest/1.0", "contains1")); + childTypeQNames.clear(); + childTypeQNames.add(ContentModel.TYPE_CONTENT); + children = filterByAssocTypeAndCheck(testFolder, assocTypeQNames, childTypeQNames); + assertEquals(1, children.size()); + + assocTypeQNames.clear(); + assocTypeQNames.add(QName.createQName("http://www.alfresco.org/test/getchildrentest/1.0", "contains1")); + assocTypeQNames.add(ContentModel.ASSOC_CONTAINS); + childTypeQNames.clear(); + childTypeQNames.add(ContentModel.TYPE_CONTENT); + children = filterByAssocTypeAndCheck(testFolder, assocTypeQNames, childTypeQNames); + assertEquals(2, children.size()); + } + // test helper method - optional filtering/sorting private PagingResults list(NodeRef parentNodeRef, final int skipCount, final int maxItems, final int requestTotalCountMax, String pattern, List> sortProps) { @@ -697,7 +750,7 @@ public class GetChildrenCannedQueryTest extends TestCase // get canned query GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject("getChildrenCannedQueryFactory"); - GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(parentNodeRef, pattern, null, null, sortProps, pagingRequest); + GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(parentNodeRef, pattern, null, null, null, sortProps, pagingRequest); // execute canned query CannedQueryResults results = cq.execute(); @@ -748,6 +801,15 @@ public class GetChildrenCannedQueryTest extends TestCase } } + private List filterByAssocTypeAndCheck(NodeRef parentNodeRef, Set assocTypeQNames, Set childTypeQNames) + { + PagingResults results = list(parentNodeRef, -1, -1, 0, assocTypeQNames, childTypeQNames, null, null); + assertTrue(results.getPage().size() > 0); + + List childNodeRefs = results.getPage(); + return childNodeRefs; + } + private void filterByPropAndCheck(NodeRef parentNodeRef, QName filterPropQName, String filterVal, FilterTypeString filterType, int expectedCount) { FilterProp filter = new FilterPropString(filterPropQName, filterVal, filterType); @@ -955,7 +1017,31 @@ public class GetChildrenCannedQueryTest extends TestCase // get canned query GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject("getChildrenCannedQueryFactory"); - GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(parentNodeRef, null, childTypeQNames, filterProps, sortProps, pagingRequest); + GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(parentNodeRef, null, null, childTypeQNames, filterProps, sortProps, pagingRequest); + + // execute canned query + CannedQueryResults results = cq.execute(); + + List nodeRefs = results.getPages().get(0); + + Integer totalCount = null; + if (requestTotalCountMax > 0) + { + totalCount = results.getTotalResultCount().getFirst(); + } + + return new PagingNodeRefResultsImpl(nodeRefs, results.hasMoreItems(), totalCount, false); + } + + // test helper method - optional filtering/sorting + private PagingResults list(NodeRef parentNodeRef, final int skipCount, final int maxItems, final int requestTotalCountMax, Set assocTypeQNames, Set childTypeQNames, List filterProps, List> sortProps) + { + PagingRequest pagingRequest = new PagingRequest(skipCount, maxItems, null); + pagingRequest.setRequestTotalCountMax(requestTotalCountMax); + + // get canned query + GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject("getChildrenCannedQueryFactory"); + GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(parentNodeRef, null, assocTypeQNames, childTypeQNames, filterProps, sortProps, pagingRequest); // execute canned query CannedQueryResults results = cq.execute(); @@ -1009,7 +1095,7 @@ public class GetChildrenCannedQueryTest extends TestCase } } - private void createFolder(NodeRef parentNodeRef, String folderName, QName folderType) throws IOException + private NodeRef createFolder(NodeRef parentNodeRef, String folderName, QName folderType) throws IOException { Map properties = new HashMap(); properties.put(ContentModel.PROP_NAME, folderName); @@ -1025,6 +1111,7 @@ public class GetChildrenCannedQueryTest extends TestCase QName.createQName(folderName), folderType, properties).getChildRef(); + return nodeRef; } private NodeRef createContent(NodeRef parentNodeRef, String fileName, QName contentType) throws IOException @@ -1053,6 +1140,32 @@ public class GetChildrenCannedQueryTest extends TestCase return nodeRef; } + private NodeRef createContent(NodeRef parentNodeRef, QName childAssocType, String fileName, QName contentType) throws IOException + { + Map properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, fileName); + properties.put(ContentModel.PROP_TITLE, fileName+" my title"); + properties.put(ContentModel.PROP_DESCRIPTION, fileName+" my description"); + + NodeRef nodeRef = nodeService.getChildByName(parentNodeRef, childAssocType, fileName); + if (nodeRef != null) + { + nodeService.deleteNode(nodeRef); + } + + nodeRef = nodeService.createNode(parentNodeRef, + childAssocType, + QName.createQName(fileName), + contentType, + properties).getChildRef(); + + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(mimetypeService.guessMimetype(fileName)); + writer.putContent("my text content"); + + return nodeRef; + } + private void loadContent(NodeRef parentNodeRef, String inFileName, String title, String description, boolean readAllowed, Set results) throws IOException { String newFileName = TEST_FILE_PREFIX + inFileName; diff --git a/source/java/org/alfresco/repo/node/getchildren/testModel.xml b/source/java/org/alfresco/repo/node/getchildren/testModel.xml new file mode 100644 index 0000000000..e981d8d6bf --- /dev/null +++ b/source/java/org/alfresco/repo/node/getchildren/testModel.xml @@ -0,0 +1,51 @@ + + + Alfresco Content Model + Alfresco + 20012-02-23 + 1.0 + + + + + + + + + + + + + + + + + + + + + Test Folder + cm:folder + true + + + + false + true + + + sys:base + false + true + + false + true + + + + + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java b/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java index a82db6d390..279d7e16b9 100644 --- a/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java +++ b/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -86,6 +86,7 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend protected void render(RenderingContext context) { ContentReader contentReader = context.makeContentReader(); + // There will have been an exception if there is no content data so contentReader is not null. String sourceUrl = contentReader.getContentUrl(); String sourceMimeType = contentReader.getMimetype(); String targetMimeType = getTargetMimeType(context); diff --git a/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneIndexerImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneIndexerImpl.java index a048659133..9c48ebc522 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneIndexerImpl.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneIndexerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -1237,9 +1237,9 @@ public class ADMLuceneIndexerImpl extends AbstractLuceneIndexerImpl imp // Content is always tokenised ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, serializableValue); - if (!index || contentData.getMimetype() == null) + if (!index || contentData == null || contentData.getMimetype() == null) { - // no mimetype or property not indexed + // no content, mimetype or property not indexed continue; } // store mimetype in index - even if content does not index it is useful diff --git a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java index c9b6c6482d..e8511695b8 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -1096,9 +1096,9 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl // Content is always tokenised ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, serializableValue); - if (!index || contentData.getMimetype() == null) + if (!index || contentData == null || contentData.getMimetype() == null) { - // no mimetype or property not indexed + // no content, mimetype or property not indexed continue; } // store mimetype in index - even if content does not index it is useful diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java index 4ce7245a2d..65308a8544 100644 --- a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -1224,7 +1224,7 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per } } - GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(contextNodeRef, null, childTypeQNames, filterProps, sortProps, pagingRequest); + GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(contextNodeRef, null, null, childTypeQNames, filterProps, sortProps, pagingRequest); // execute canned query final CannedQueryResults results = cq.execute(); diff --git a/source/java/org/alfresco/repo/site/SiteServiceImpl.java b/source/java/org/alfresco/repo/site/SiteServiceImpl.java index 9c90a5f377..d67b97e982 100644 --- a/source/java/org/alfresco/repo/site/SiteServiceImpl.java +++ b/source/java/org/alfresco/repo/site/SiteServiceImpl.java @@ -924,7 +924,7 @@ public class SiteServiceImpl extends AbstractLifecycleBean implements SiteServic final String cQBeanName = "siteGetChildrenCannedQueryFactory"; GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject(cQBeanName); - GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(getSiteRoot(), null, searchTypeQNames, + GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(getSiteRoot(), null, null, searchTypeQNames, filterProps, sortProps, pagingRequest); // execute canned query diff --git a/source/java/org/alfresco/repo/template/BaseContentNode.java b/source/java/org/alfresco/repo/template/BaseContentNode.java index baf660b8f4..21cd898b27 100644 --- a/source/java/org/alfresco/repo/template/BaseContentNode.java +++ b/source/java/org/alfresco/repo/template/BaseContentNode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -593,6 +593,10 @@ public abstract class BaseContentNode implements TemplateContent // get the content reader ContentService contentService = services.getContentService(); ContentReader reader = contentService.getReader(getNodeRef(), property); + if (reader == null) + { + return ""; // Caller of this method returns "" if there is an IOException + } // get the writer and set it up for text convert ContentWriter writer = contentService.getTempWriter(); diff --git a/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java b/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java index e899aa69e6..d48fdb3fde 100644 --- a/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java +++ b/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -44,7 +44,7 @@ import org.alfresco.repo.security.authentication.AuthenticationContext; import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.site.SiteAVMBootstrap; +import org.alfresco.repo.thumbnail.ThumbnailRegistry; import org.alfresco.repo.usage.UserUsageTrackingComponent; import org.alfresco.repo.workflow.WorkflowDeployer; import org.alfresco.service.cmr.admin.RepoAdminService; @@ -90,6 +90,7 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo private AttributeService attributeService; private PasswordEncoder passwordEncoder; private TenantRoutingFileContentStore tenantFileContentStore; + private ThumbnailRegistry thumbnailRegistry; private WorkflowService workflowService; private RepositoryExporterService repositoryExporterService; private ModuleService moduleService; @@ -197,6 +198,11 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo this.moduleService = moduleService; } + public void setThumbnailRegistry(ThumbnailRegistry thumbnailRegistry) + { + this.thumbnailRegistry = thumbnailRegistry; + } + public void setBaseAdminUsername(String baseAdminUsername) { this.baseAdminUsername = baseAdminUsername; @@ -365,6 +371,8 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo ImporterBootstrap spacesImporterBootstrap = (ImporterBootstrap)ctx.getBean("spacesBootstrap-mt"); bootstrapSpacesTenantStore(spacesImporterBootstrap, tenantDomain); + thumbnailRegistry.initThumbnailDefinitions(); + // notify listeners that tenant has been created & hence enabled for (TenantDeployer tenantDeployer : tenantDeployers) { @@ -433,6 +441,8 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo importBootstrapSpacesModelsTenantStore(tenantDomain, directorySource); importBootstrapSpacesTenantStore(tenantDomain, directorySource); + thumbnailRegistry.initThumbnailDefinitions(); + // notify listeners that tenant has been created & hence enabled for (TenantDeployer tenantDeployer : tenantDeployers) { diff --git a/source/java/org/alfresco/repo/thumbnail/SimpleThumbnailer.java b/source/java/org/alfresco/repo/thumbnail/SimpleThumbnailer.java index a8ac7b6967..96e5af4bf1 100644 --- a/source/java/org/alfresco/repo/thumbnail/SimpleThumbnailer.java +++ b/source/java/org/alfresco/repo/thumbnail/SimpleThumbnailer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -156,48 +156,51 @@ public class SimpleThumbnailer extends TransactionListenerAdapter implements } Serializable value = this.nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, value); - List thumbnailDefinitions = this.thumbnailService.getThumbnailRegistry() - .getThumbnailDefinitions(contentData.getMimetype(), contentData.getSize()); - for (final ThumbnailDefinition thumbnailDefinition : thumbnailDefinitions) + if (contentData != null) { - final NodeRef existingThumbnail = this.thumbnailService.getThumbnailByName(nodeRef, - ContentModel.PROP_CONTENT, thumbnailDefinition.getName()); - try + List thumbnailDefinitions = this.thumbnailService.getThumbnailRegistry() + .getThumbnailDefinitions(contentData.getMimetype(), contentData.getSize()); + for (final ThumbnailDefinition thumbnailDefinition : thumbnailDefinitions) { - // Generate each thumbnail in its own transaction, so that we can recover if one of them goes wrong - this.transactionService.getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionCallback() - { - - public Object execute() throws Throwable + final NodeRef existingThumbnail = this.thumbnailService.getThumbnailByName(nodeRef, + ContentModel.PROP_CONTENT, thumbnailDefinition.getName()); + try + { + // Generate each thumbnail in its own transaction, so that we can recover if one of them goes wrong + this.transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() { - if (existingThumbnail == null) + + public Object execute() throws Throwable { - if (SimpleThumbnailer.logger.isDebugEnabled()) + if (existingThumbnail == null) { - SimpleThumbnailer.logger.debug("Creating thumbnail \"" - + thumbnailDefinition.getName() + "\" for node " + nodeRef.getId()); + if (SimpleThumbnailer.logger.isDebugEnabled()) + { + SimpleThumbnailer.logger.debug("Creating thumbnail \"" + + thumbnailDefinition.getName() + "\" for node " + nodeRef.getId()); + } + SimpleThumbnailer.this.thumbnailService.createThumbnail(nodeRef, + ContentModel.PROP_CONTENT, thumbnailDefinition.getMimetype(), + thumbnailDefinition.getTransformationOptions(), thumbnailDefinition + .getName()); } - SimpleThumbnailer.this.thumbnailService.createThumbnail(nodeRef, - ContentModel.PROP_CONTENT, thumbnailDefinition.getMimetype(), - thumbnailDefinition.getTransformationOptions(), thumbnailDefinition - .getName()); + else + { + SimpleThumbnailer.logger.debug("Updating thumbnail \"" + + thumbnailDefinition.getName() + "\" for node " + nodeRef.getId()); + SimpleThumbnailer.this.thumbnailService.updateThumbnail(existingThumbnail, + thumbnailDefinition.getTransformationOptions()); + } + return null; } - else - { - SimpleThumbnailer.logger.debug("Updating thumbnail \"" - + thumbnailDefinition.getName() + "\" for node " + nodeRef.getId()); - SimpleThumbnailer.this.thumbnailService.updateThumbnail(existingThumbnail, - thumbnailDefinition.getTransformationOptions()); - } - return null; - } - }, false, true); - } - catch (Exception e) - { - SimpleThumbnailer.logger.warn("Failed to generate thumbnail \"" + thumbnailDefinition.getName() - + "\" for node " + nodeRef.getId(), e); + }, false, true); + } + catch (Exception e) + { + SimpleThumbnailer.logger.warn("Failed to generate thumbnail \"" + thumbnailDefinition.getName() + + "\" for node " + nodeRef.getId(), e); + } } } } diff --git a/source/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java b/source/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java index a144a11e76..2b43de6e3c 100644 --- a/source/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java +++ b/source/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java @@ -27,6 +27,8 @@ import org.alfresco.repo.content.transform.ContentTransformer; import org.alfresco.repo.content.transform.TransformerDebug; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.tenant.Tenant; +import org.alfresco.repo.tenant.TenantAdminService; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.cmr.rendition.RenditionDefinition; import org.alfresco.service.cmr.rendition.RenditionService; @@ -63,6 +65,8 @@ public class ThumbnailRegistry implements ApplicationContextAware, ApplicationLi /** Rendition service */ private RenditionService renditionService; + private TenantAdminService tenantAdminService; + /** Map of thumbnail definition */ private Map thumbnailDefinitions = new HashMap(); @@ -113,6 +117,11 @@ public class ThumbnailRegistry implements ApplicationContextAware, ApplicationLi { this.renditionService = renditionService; } + + public void setTenantAdminService(TenantAdminService tenantAdminService) + { + this.tenantAdminService = tenantAdminService; + } /** * This method is used to inject the thumbnail definitions. @@ -132,50 +141,28 @@ public class ThumbnailRegistry implements ApplicationContextAware, ApplicationLi } } - /** - * Those thumbnail definitions that are injected by Spring are converted - * to rendition definitions and saved. - */ - private void initThumbnailDefinitions() + // Otherwise we should go ahead and persist the thumbnail definitions. + // This is done during system startup. It needs to be done as the system user (see callers) to ensure the thumbnail definitions get saved + // and also needs to be done within a transaction in order to support concurrent startup. See ALF-6271 for details. + public void initThumbnailDefinitions() { - // If the database is in read-only mode, then do not persist the thumbnail definitions. - if (transactionService.isReadOnly()) - { - if (logger.isDebugEnabled()) - { - logger.debug("TransactionService is in read-only mode. Therefore no thumbnail definitions have been initialised."); - } - return; - } - - // Otherwise we should go ahead and persist the thumbnail definitions. - // This is done during system startup. It needs to be done as the system user to ensure the thumbnail definitions get saved - // and also needs to be done within a transaction in order to support concurrent startup. See ALF-6271 for details. RetryingTransactionHelper transactionHelper = transactionService.getRetryingTransactionHelper(); transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() { @Override public Void execute() throws Throwable { - AuthenticationUtil.runAs(new RunAsWork() { - public Void doWork() throws Exception - { - for (String thumbnailDefName : thumbnailDefinitions.keySet()) - { - final ThumbnailDefinition thumbnailDefinition = thumbnailDefinitions.get(thumbnailDefName); - - // Built-in thumbnailDefinitions do not provide any non-standard values - // for the ThumbnailParentAssociationDetails object. Hence the null - RenditionDefinition renditionDef = thumbnailRenditionConvertor.convert(thumbnailDefinition, null); - - // Thumbnail definitions are saved into the repository as actions - renditionService.saveRenditionDefinition(renditionDef); - } - - return null; - } - }, AuthenticationUtil.getSystemUserName()); - + for (String thumbnailDefName : thumbnailDefinitions.keySet()) + { + final ThumbnailDefinition thumbnailDefinition = thumbnailDefinitions.get(thumbnailDefName); + + // Built-in thumbnailDefinitions do not provide any non-standard values + // for the ThumbnailParentAssociationDetails object. Hence the null + RenditionDefinition renditionDef = thumbnailRenditionConvertor.convert(thumbnailDefinition, null); + + // Thumbnail definitions are saved into the repository as actions + renditionService.saveRenditionDefinition(renditionDef); + } return null; } }); @@ -366,7 +353,47 @@ public class ThumbnailRegistry implements ApplicationContextAware, ApplicationLi @Override protected void onBootstrap(ApplicationEvent event) { - initThumbnailDefinitions(); + long start = System.currentTimeMillis(); + + // If the database is in read-only mode, then do not persist the thumbnail definitions. + if (transactionService.isReadOnly()) + { + if (logger.isDebugEnabled()) + { + logger.debug("TransactionService is in read-only mode. Therefore no thumbnail definitions have been initialised."); + } + return; + } + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + initThumbnailDefinitions(); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + if (tenantAdminService.isEnabled()) + { + List tenants = tenantAdminService.getAllTenants(); + for (Tenant tenant : tenants) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + initThumbnailDefinitions(); + return null; + } + }, tenantAdminService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenant.getTenantDomain())); + } + } + + if (logger.isInfoEnabled()) + { + logger.info("Init'ed thumbnail defs in "+(System.currentTimeMillis()-start)+" ms"); + } } /* (non-Javadoc) diff --git a/source/java/org/alfresco/util/ValueProtectingMap.java b/source/java/org/alfresco/util/ValueProtectingMap.java new file mode 100644 index 0000000000..a5c41ca000 --- /dev/null +++ b/source/java/org/alfresco/util/ValueProtectingMap.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * A map that protects keys and values from accidental modification. + *

    + * Use this map when keys or values need to be protected against client modification. + * For example, when a component pulls a map from a common resource it can wrap + * the map with this class to prevent any accidental modification of the shared + * resource. + *

    + * Upon first write to this map , the underlying map will be copied (selectively cloned), + * the original map handle will be discarded and the copied map will be used. Note that + * the map copy process will also occur if any mutable value is in danger of being + * exposed to client modification. Therefore, methods that iterate and retrieve values + * will also trigger the copy if any values are mutable. + * + * @param the map key type (must extend {@link Serializable}) + * @param the map value type (must extend {@link Serializable}) + * + * @author Derek Hulley + * @since 3.4.9 + * @since 4.0.1 + */ +public class ValueProtectingMap implements Map +{ + /** + * Default immutable classes: + *

  • String
  • + *
  • BigDecimal
  • + *
  • BigInteger
  • + *
  • Byte
  • + *
  • Double
  • + *
  • Float
  • + *
  • Integer
  • + *
  • Long
  • + *
  • Short
  • + *
  • Boolean
  • + *
  • Date
  • + *
  • Locale
  • + */ + public static final Set> DEFAULT_IMMUTABLE_CLASSES; + static + { + DEFAULT_IMMUTABLE_CLASSES = new HashSet>(13); + DEFAULT_IMMUTABLE_CLASSES.add(String.class); + DEFAULT_IMMUTABLE_CLASSES.add(BigDecimal.class); + DEFAULT_IMMUTABLE_CLASSES.add(BigInteger.class); + DEFAULT_IMMUTABLE_CLASSES.add(Byte.class); + DEFAULT_IMMUTABLE_CLASSES.add(Double.class); + DEFAULT_IMMUTABLE_CLASSES.add(Float.class); + DEFAULT_IMMUTABLE_CLASSES.add(Integer.class); + DEFAULT_IMMUTABLE_CLASSES.add(Long.class); + DEFAULT_IMMUTABLE_CLASSES.add(Short.class); + DEFAULT_IMMUTABLE_CLASSES.add(Boolean.class); + DEFAULT_IMMUTABLE_CLASSES.add(Date.class); + DEFAULT_IMMUTABLE_CLASSES.add(Locale.class); + } + + /** + * Protect a specific value if it is considered mutable + * + * @param the type of the value, which must be {@link Serializable} + * @param value the value to protect if it is mutable (may be null) + * @param immutableClasses a set of classes that can be considered immutable + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + * @return a cloned instance (via serialization) or the instance itself, if immutable + */ + @SuppressWarnings("unchecked") + public static S protectValue(S value, Set> immutableClasses) + { + if (!mustProtectValue(value, immutableClasses)) + { + return value; + } + // We have to clone it + // No worries about the return type; it has to be the same as we put into the serializer + return (S) SerializationUtils.deserialize(SerializationUtils.serialize(value)); + } + + /** + * Utility method to check if values need to be cloned or not + * + * @param the type of the value, which must be {@link Serializable} + * @param value the value to check + * @param immutableClasses a set of classes that can be considered immutable + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + * @return true if the value must NOT be given + * to the calling clients + */ + public static boolean mustProtectValue(S value, Set> immutableClasses) + { + if (value == null) + { + return false; + } + Class clazz = value.getClass(); + return ( + DEFAULT_IMMUTABLE_CLASSES.contains(clazz) == false && + immutableClasses.contains(clazz) == false); + } + + /** + * Utility method to clone a map, preserving immutable instances + * + * @param the map key type, which must be {@link Serializable} + * @param the map value type, which must be {@link Serializable} + * @param map the map to copy + * @param immutableClasses a set of classes that can be considered immutable + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + */ + public static Map cloneMap(Map map, Set> immutableClasses) + { + Map copy = new HashMap((int)(map.size() * 1.3)); + for (Map.Entry element : map.entrySet()) + { + K key = element.getKey(); + V value = element.getValue(); + // Clone as necessary + key = ValueProtectingMap.protectValue(key, immutableClasses); + value = ValueProtectingMap.protectValue(value, immutableClasses); + copy.put(key, value); + } + return copy; + } + + private ReentrantReadWriteLock.ReadLock readLock; + private ReentrantReadWriteLock.WriteLock writeLock; + + private boolean cloned = false; + private Map map; + private Set> immutableClasses; + + /** + * Construct providing a protected map and using only the + * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} + * + * @param protectedMap the map to safeguard + */ + public ValueProtectingMap(Map protectedMap) + { + this (protectedMap, null); + } + + /** + * Construct providing a protected map, complementing the set of + * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} + * + * @param protectedMap the map to safeguard + * @param immutableClasses additional immutable classes + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + * (may be null + */ + public ValueProtectingMap(Map protectedMap, Set> immutableClasses) + { + // Unwrap any internal maps if given a value protecting map + if (protectedMap instanceof ValueProtectingMap) + { + ValueProtectingMap mapTemp = (ValueProtectingMap) protectedMap; + this.map = mapTemp.map; + } + else + { + this.map = protectedMap; + } + + this.cloned = false; + if (immutableClasses == null) + { + this.immutableClasses = Collections.emptySet(); + } + else + { + this.immutableClasses = new HashSet>(immutableClasses); + } + // Construct locks + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + this.readLock = lock.readLock(); + this.writeLock = lock.writeLock(); + } + + /** + * An unsafe method to use for anything except tests. + * + * @return the map that this instance is protecting + */ + /* protected */ Map getProtectedMap() + { + return map; + } + + /** + * Called by methods that need to force the map into a safe state. + *

    + * This method can be called without any locks being active. + */ + private void cloneMap() + { + readLock.lock(); + try + { + // Check that it hasn't been copied already + if (cloned) + { + return; + } + } + finally + { + readLock.unlock(); + } + /* + * Note: This space here is a window during which some code could have made + * a copy. Therefore we will do a cautious double-check. + */ + // Put in a write lock before cloning the map + writeLock.lock(); + try + { + // Check that it hasn't been copied already + if (cloned) + { + return; + } + + Map copy = ValueProtectingMap.cloneMap(map, immutableClasses); + // Discard the original + this.map = copy; + this.cloned = true; + } + finally + { + writeLock.unlock(); + } + } + + /* + * READ-ONLY METHODS + */ + + @Override + public int size() + { + readLock.lock(); + try + { + return map.size(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean isEmpty() + { + readLock.lock(); + try + { + return map.isEmpty(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean containsKey(Object key) + { + readLock.lock(); + try + { + return map.containsKey(key); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean containsValue(Object value) + { + readLock.lock(); + try + { + return map.containsValue(value); + } + finally + { + readLock.unlock(); + } + + } + + @Override + public int hashCode() + { + readLock.lock(); + try + { + return map.hashCode(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean equals(Object obj) + { + readLock.lock(); + try + { + return map.equals(obj); + } + finally + { + readLock.unlock(); + } + } + + @Override + public String toString() + { + readLock.lock(); + try + { + return map.toString(); + } + finally + { + readLock.unlock(); + } + } + + /* + * METHODS THAT *MIGHT* REQUIRE COPY + */ + + @Override + public V get(Object key) + { + readLock.lock(); + try + { + V value = map.get(key); + return ValueProtectingMap.protectValue(value, immutableClasses); + } + finally + { + readLock.unlock(); + } + } + + /* + * METHODS THAT REQUIRE COPY + */ + + @Override + public V put(K key, V value) + { + cloneMap(); + return map.put(key, value); + } + + @Override + public V remove(Object key) + { + cloneMap(); + return map.remove(key); + } + + @Override + public void putAll(Map m) + { + cloneMap(); + map.putAll(m); + } + + @Override + public void clear() + { + cloneMap(); + map.clear(); + } + + @Override + public Set keySet() + { + cloneMap(); + return map.keySet(); + } + + @Override + public Collection values() + { + cloneMap(); + return map.values(); + } + + @Override + public Set> entrySet() + { + cloneMap(); + return map.entrySet(); + } +} diff --git a/source/java/org/alfresco/util/ValueProtectingMapTest.java b/source/java/org/alfresco/util/ValueProtectingMapTest.java new file mode 100644 index 0000000000..14f32cc668 --- /dev/null +++ b/source/java/org/alfresco/util/ValueProtectingMapTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +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 junit.framework.TestCase; + +/** + * Tests {@link ValueProtectingMap} + * + * @author Derek Hulley + * @since 3.4.9 + * @since 4.0.1 + */ +public class ValueProtectingMapTest extends TestCase +{ + private static Set> moreImmutableClasses; + static + { + moreImmutableClasses = new HashSet>(13); + moreImmutableClasses.add(TestImmutable.class); + } + + /** + * A class that is immutable + */ + @SuppressWarnings("serial") + private static class TestImmutable implements Serializable + { + } + + /** + * A class that is mutable + */ + @SuppressWarnings("serial") + private static class TestMutable extends TestImmutable + { + public int i = 0; + public void increment() + { + i++; + } + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + TestMutable other = (TestMutable) obj; + if (i != other.i) return false; + return true; + } + } + + private List valueList; + private Map valueMap; + private Date valueDate; + private TestImmutable valueImmutable; + private TestMutable valueMutable; + + private ValueProtectingMap map; + private Map holyMap; + + @Override + protected void setUp() throws Exception + { + valueList = new ArrayList(4); + valueList.add("ONE"); + valueList.add("TWO"); + valueList.add("THREE"); + valueList.add("FOUR"); + valueList = Collections.unmodifiableList(valueList); + + valueMap = new HashMap(5); + valueMap.put("ONE", "ONE"); + valueMap.put("TWO", "TWO"); + valueMap.put("THREE", "THREE"); + valueMap.put("FOUR", "FOUR"); + valueMap = Collections.unmodifiableMap(valueMap); + + valueDate = new Date(); + + valueImmutable = new TestImmutable(); + valueMutable = new TestMutable(); + + holyMap = new HashMap(); + holyMap.put("DATE", valueDate); + holyMap.put("LIST", (Serializable) valueList); + holyMap.put("MAP", (Serializable) valueMap); + holyMap.put("IMMUTABLE", valueImmutable); + holyMap.put("MUTABLE", valueMutable); + + // Now wrap our 'holy' map so that it cannot be modified + holyMap = Collections.unmodifiableMap(holyMap); + + map = new ValueProtectingMap(holyMap, moreImmutableClasses); + } + + /** + * Make sure that NOTHING has changed in our 'holy' map + */ + private void checkMaps(boolean expectMapClone) + { + assertEquals("Holy map size is wrong: ", 5, holyMap.size()); + // Note that the immutability of the maps and lists means that we don't need + // to check every value within the lists and maps + if (expectMapClone) + { + // Make sure that the holy map has been released + assertTrue("Expect holy map to have been released: ", map.getProtectedMap() != holyMap); + // Do some updates to the backing map and ensure that they stick + Map mapClone = map.getProtectedMap(); + mapClone.put("ONE", "ONE"); + assertEquals("Modified the backing directly but value is not visible: ", map.get("ONE"), "ONE"); + map.put("TWO", "TWO"); + assertTrue("Backing map was changed again!", mapClone == map.getProtectedMap()); + mapClone.containsKey("TWO"); + } + else + { + // Make sure that the holy map is still acting as the backing map + assertTrue("Expect holy map to still be in use: ", map.getProtectedMap() == holyMap); + } + } + + public void testSetup() + { + checkMaps(false); + } + + /** + * No matter how many times we wrap instances in instances, the backing map must remain + * the same. + */ + public void testMapWrapping() + { + ValueProtectingMap mapTwo = new ValueProtectingMap(map); + assertTrue("Backing map must be shared: ", mapTwo.getProtectedMap() == map.getProtectedMap()); + ValueProtectingMap mapThree = new ValueProtectingMap(map); + assertTrue("Backing map must be shared: ", mapThree.getProtectedMap() == map.getProtectedMap()); + } + + public void testMapClear() + { + map.clear(); + assertEquals("Map should be empty: ", 0, map.size()); + checkMaps(true); + } + + public void testMapContainsKey() + { + assertTrue(map.containsKey("LIST")); + assertFalse(map.containsKey("LISTXXX")); + checkMaps(false); + } + + public void testMapContainsValue() + { + assertTrue(map.containsValue(valueMutable)); + assertFalse(map.containsValue("Dassie")); + checkMaps(false); + } + + public void testMapEntrySet() + { + map.entrySet(); + checkMaps(true); + } + + /** + * Ensures that single, immutable values are given out as-is and + * without affecting the backing storage + */ + public void testMapGetImmutable() + { + assertTrue("Immutable value instance incorrect", map.get("IMMUTABLE") == valueImmutable); + checkMaps(false); + } + + /** + * Ensures that single, immutable values are cloned before being given out + * without affecting the backing storage + */ + public void testMapGetMutable() + { + TestMutable mutable = (TestMutable) map.get("MUTABLE"); + assertFalse("Mutable value instance incorrect", mutable == valueMutable); + checkMaps(false); + // Modify the instance + mutable.increment(); + assertEquals("Backing mutable should not have changed: ", 0, valueMutable.i); + } + + public void testMapIsEmpty() + { + assertFalse(map.isEmpty()); + checkMaps(false); + } + + public void testMapKeySet() + { + map.keySet(); + checkMaps(true); + } + + public void testMapPut() + { + map.put("ANOTHER", "VALUE"); + checkMaps(true); + } + + public void testMapPutAll() + { + map.putAll(holyMap); + checkMaps(true); + } +}