1/*
2 * Copyright 2004-2005 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import org.tmatesoft.svn.core.io.SVNRepositoryFactory
18import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory
19import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory
20import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl
21import org.tmatesoft.svn.core.io.*
22import org.tmatesoft.svn.core.*
23import org.tmatesoft.svn.core.auth.*
24import org.tmatesoft.svn.core.wc.*
25import org.codehaus.groovy.grails.documentation.MetadataGeneratingMetaClassCreationHandle
26import org.codehaus.groovy.grails.plugins.publishing.DefaultPluginPublisher
27import org.springframework.core.io.FileSystemResource
28import groovy.xml.DOMBuilder
29import groovy.xml.dom.DOMCategory
30import javax.xml.transform.TransformerFactory
31import javax.xml.transform.OutputKeys
32import javax.xml.transform.Transformer
33import javax.xml.transform.dom.DOMSource
34import javax.xml.transform.stream.StreamResult
35import org.codehaus.groovy.grails.plugins.PluginManagerHolder
36
37/**
38 * Gant script that handles releasing plugins to a plugin repository.
39 *
40 * @author Graeme Rocher
41 */
42
43includeTargets << grailsScript("_GrailsPluginDev")
44includeTargets << grailsScript("_GrailsBootstrap")
45includeTargets << grailsScript("_GrailsDocs")
46
47authManager = null
48commitMessage = null
49trunk = null
50latestRelease = null
51versionedRelease = null
52skipLatest = false
53
54CORE_PLUGIN_DIST = "http://svn.codehaus.org/grails/trunk/grails-plugins"
55CORE_PUBLISH_URL = "https://svn.codehaus.org/grails/trunk/grails-plugins"
56DEFAULT_PLUGIN_DIST = "http://plugins.grails.org"
57DEFAULT_PUBLISH_URL = "https://svn.codehaus.org/grails-plugins"
58
59// setup default plugin respositories for discovery
60pluginDiscoveryRepositories = [core:CORE_PLUGIN_DIST, default:DEFAULT_PLUGIN_DIST]
61if (grailsSettings?.config?.grails?.plugin?.repos?.discovery) {
62    pluginDiscoveryRepositories.putAll(grailsSettings?.config?.grails?.plugin?.repos?.discovery)
63}
64
65// setup default plugin respositories for publishing
66pluginDistributionRepositories = [core:CORE_PUBLISH_URL, default:DEFAULT_PUBLISH_URL]
67if (grailsSettings?.config?.grails?.plugin?.repos?.distribution) {
68    pluginDistributionRepositories.putAll(grailsSettings?.config?.grails?.plugin?.repos?.distribution)
69}
70
71KEY_URL = "url"
72KEY_USER_NAME = "user"
73KEY_USER_PASS = "pswd"
74
75/**
76 * authentication manager Map
77 * EG : INIT CORE OR DEFAULT DISTRIBUTION REPOSITORY
78 * aMap = [url:CORE_PUBLISH_URL,user:"auser",pswd:"apswd"]
79 * ALLOWS ANT PROPERTIES DEFINITION
80 * aAuthManager = getAuthenticationManager("core", "distribution", aMap)
81 * authManagerMap.put("distribution.core", aAuthManager)
82 */
83authManagerMap = [:]
84
85/**
86 * Break down url into separate authentication components from url
87 * @param url to be parsed
88 * @return broken down url in these parts (if exist) url, user and pswd
89 */
90public Map tokenizeUrl(String url) throws SVNException {
91    SVNURL aURL= SVNURL.parseURIDecoded(url)
92    def aHashMap  = [:]
93
94    def userInfo = aURL.userInfo
95    if (userInfo) {
96        def userInfoArray = userInfo.split(":")
97        aHashMap[KEY_USER_NAME] = userInfoArray[0]
98        if (userInfoArray.length>1) {
99            aHashMap[KEY_USER_PASS] = userInfoArray[1]
100        }
101    }
102    if (aURL.port == 443) {
103        aHashMap[KEY_URL] = "${aURL.protocol}://${aURL.host}${aURL.path}".toString()
104    }
105    else {
106        aHashMap[KEY_URL] = "${aURL.protocol}://${aURL.host}:${aURL.port}${aURL.path}".toString()
107    }
108
109    return aHashMap
110}
111
112//init SVN Kit one time
113// support file based SVN
114FSRepositoryFactory.setup()
115// support the server http/https
116DAVRepositoryFactory.setup()
117// support svn protocol
118SVNRepositoryFactoryImpl.setup()
119
120/**
121 * Replace the url with authentication by url without in discovery and distribution
122 * repository and setup authentication instance in authManagerMap.
123 * if an url like :
124 * {protocol}://{user:password}@url is defined the promt does not occur.
125 * Else the user is prompted for user and password values.
126 * The repos "core" and "default" are ignored (see above for default configuration)
127 * For all other repo, a defultAuthenticationManager is created.
128 */
129public Map configureAuth(Map repoMap,String repoType) {
130    repoMapTmp = [:]
131    repoMap.each {
132        ISVNAuthenticationManager aAuthManager = SVNWCUtil.createDefaultAuthenticationManager()
133        if ("core" == it.key || "default" == it.key) {
134            repoMapTmp[it.key] = it.value
135            if (!isSecureUrl(it.value)) {
136                authManagerMap[repoType+"."+it.key] = aAuthManager
137            }
138        // else no authentication manager provides to authManagerMap : case of CORE_PUBLISH_URL and DEFAULT_PUBLISH_URL
139        }
140        else {
141            if (isSecureUrl(it.value)) {
142                event "StatusUpdate", ["Authentication for svn repo at ${it.key} ${repoType} is required."]
143                aMap = tokenizeUrl(it.value)
144                repoMapTmp[it.key] = aMap[KEY_URL]
145                aAuthManager = getAuthenticationManager(it.key, repoType, aMap)
146                authManagerMap[repoType + "." + it.key] = aAuthManager
147            }
148            else {
149                repoMapTmp[it.key] = it.value
150                event "StatusUpdate", ["No authentication for svn repo at ${it.key}"]
151                authManagerMap[repoType + "." + it.key] = aAuthManager
152            }
153        }
154    }
155
156    return repoMapTmp
157}
158
159//configure authentication for discovery repository
160pluginDiscoveryRepositories = configureAuth(pluginDiscoveryRepositories, "discovery")
161
162//configure authentication for distribution repository
163pluginDistributionRepositories = configureAuth(pluginDistributionRepositories, "distribution")
164
165/**
166 * Provide an authentication manager object.
167 * This method tries to use the configuration in settings.groovy
168 * (see comment of configureAuth for configuration pattern). If
169 * there's no configuration the user is prompted.
170 * @param repoKey
171 * @param repoType discovery or distribution
172 * @param tokenizeUrl the broken down url
173 * @return an ISVNAuthenticationManager impl instance building
174 */
175private ISVNAuthenticationManager getAuthenticationManager(String repoKey, String repoType, Map tokenizeUrl) {
176    ISVNAuthenticationManager aAuthManager
177    usr = "user.svn.username.${repoType}.${repoKey}".toString()
178    psw = "user.svn.password.${repoType}.${repoKey}".toString()
179    if (tokenizeUrl[KEY_USER_NAME]) {
180        ant.antProject.setNewProperty usr, ""+tokenizeUrl[KEY_USER_NAME]
181        String pswd = tokenizeUrl[KEY_USER_PASS] ?: ""
182        ant.antProject.setNewProperty(psw, pswd)
183    }
184    //If no provided info, the user have to be prompt
185    ant.input(message:"Please enter your SVN username:", addproperty:usr)
186    ant.input(message:"Please enter your SVN password:", addproperty:psw)
187    def username = ant.antProject.getProperty(usr)
188    def password = ant.antProject.getProperty(psw)
189    authManager = SVNWCUtil.createDefaultAuthenticationManager(username , password)
190    //Test connection
191    aUrl = tokenizeUrl[KEY_URL]
192    def svnUrl = SVNURL.parseURIEncoded(aUrl)
193    def repo = SVNRepositoryFactory.create(svnUrl, null)
194    repo.authenticationManager = authManager
195    try {
196        repo.testConnection()
197        //only if it works...
198        repo.closeSession()
199    }
200    catch (SVNAuthenticationException ex) {
201        //GRAVE BAD CONFIGURATION :  EXITING
202        event("StatusError",["Bad authentication configuration for $aUrl"])
203        exit(1)
204    }
205    return authManager
206}
207
208/**
209 * Get an authenticationManager object starting from url
210 * @param url of SVN repo
211 * @param repoType discovery or distribution
212 * @return ISVNAuthenticationManager for SVN connection on on repo with auth
213 **/
214private ISVNAuthenticationManager getAuthFromUrl(url, repoType) {
215    keyValue = repoType + "." + pluginDiscoveryRepositories.find { url.startsWith(it.value) }?.key
216    return authManagerMap[keyValue]
217}
218
219configureRepository =  { targetRepoURL, String alias = "default" ->
220    repositoryName = alias
221    pluginsList = null
222    pluginsListFile = new File(grailsSettings.grailsWorkDir, "plugins-list-${alias}.xml")
223
224    def namedPluginSVN = pluginDistributionRepositories.find { it.key == alias }?.value
225    if (namedPluginSVN) {
226        pluginSVN = namedPluginSVN
227    }
228    else {
229        pluginSVN = DEFAULT_PUBLISH_URL
230    }
231    pluginDistURL = targetRepoURL
232    pluginBinaryDistURL = "$targetRepoURL/dist"
233    remotePluginList = "$targetRepoURL/.plugin-meta/plugins-list.xml"
234}
235
236configureRepository(DEFAULT_PLUGIN_DIST)
237
238configureRepositoryForName = { String targetRepository, type="discovery" ->
239    // Works around a bug in Groovy 1.5.6's DOMCategory that means get on Object returns null. Change to "pluginDiscoveryRepositories.targetRepository" when upgrading
240    def targetRepoURL = pluginDiscoveryRepositories.find { it.key == targetRepository }?.value
241
242    if (targetRepoURL) {
243        configureRepository(targetRepoURL, targetRepository)
244    }
245    else {
246        println "No repository configured for name ${targetRepository}. Set the 'grails.plugin.repos.${type}.${targetRepository}' variable to the location of the repository."
247        exit(1)
248    }
249}
250
251target ('default': "A target for plug-in developers that uploads and commits the current plug-in as the latest revision. The command will prompt for your SVN login details.") {
252    releasePlugin()
253}
254
255target(processAuth:"Prompts user for login details to create authentication manager") {
256    if (!authManager) {
257        def authKey = argsMap.repository ? "distribution.${argsMap.repository}".toString() : "distribution.default"
258        if (authManagerMap[authKey]) {
259            authManager = authManagerMap[authKey]
260        }
261        else {
262            usr = "user.svn.username.distribution"
263            psw = "user.svn.password.distribution"
264            if (argsMap.repository) {
265                usr = usr + "." + argsMap.repository
266                psw = psw + "." + argsMap.repository
267            }
268            else {
269                usr = usr + ".default"
270                psw = psw + ".default"
271            }
272
273            def (username, password) = [argsMap.username, argsMap.password]
274
275            if (!username) {
276                ant.input(message:"Please enter your SVN username:", addproperty:usr)
277                username = ant.antProject.getProperty(usr)
278            }
279
280            if (!password) {
281                ant.input(message:"Please enter your SVN password:", addproperty:psw)
282                password = ant.antProject.getProperty(psw)
283            }
284
285            authManager = SVNWCUtil.createDefaultAuthenticationManager(username , password)
286            authManagerMap.put(authKey,authManager)
287        }
288    }
289}
290
291target(checkLicense:"Checks the license file for the plugin exists") {
292    if (!(new File("${basedir}/LICENSE").exists()) && !(new File("${basedir}/LICENSE.txt").exists())) {
293        println "No LICENSE.txt file for plugin found. Please provide a license file containing the appropriate software licensing information (eg. Apache 2.0, GPL etc.)"
294        exit(1)
295    }
296}
297
298target(releasePlugin: "The implementation target") {
299    depends(parseArguments,checkLicense)
300
301    if (argsMap.skipMetadata != true) {
302        println "Generating plugin project behavior metadata..."
303        try {
304            MetadataGeneratingMetaClassCreationHandle.enable()
305            packageApp()
306            loadApp()
307            configureApp()
308            MetadataGeneratingMetaClassCreationHandle.disable()
309        }
310        catch (e) {
311            println "There was an error generating project behavior metadata: [${e.message}]"
312        }
313    }
314    packagePlugin()
315    if (argsMap.skipDocs != true) {
316        docs()
317    }
318
319    if (argsMap.packageOnly) {
320        return
321    }
322    processAuth()
323
324    if (argsMap.message) {
325        commitMessage = argsMap.message
326    }
327
328    if (argsMap.repository) {
329        configureRepositoryForName(argsMap.repository, "distribution")
330    }
331    if (argsMap.snapshot || argsMap.'skip-latest') {
332        skipLatest = true
333    }
334    remoteLocation = "${pluginSVN}/grails-${pluginName}"
335    trunk = SVNURL.parseURIDecoded("${remoteLocation}/trunk")
336    latestRelease = "${remoteLocation}/tags/LATEST_RELEASE"
337    versionedRelease = "${remoteLocation}/tags/RELEASE_${plugin.version.toString().replaceAll('\\.','_')}"
338
339    FSRepositoryFactory.setup()
340    DAVRepositoryFactory.setup()
341    SVNRepositoryFactoryImpl.setup()
342
343    try {
344        if (argsMap.pluginlist) {
345            commitNewGlobalPluginList()
346        }
347        else if (argsMap.zipOnly) {
348            publishZipOnlyRelease()
349        }
350        else {
351            def statusClient = new SVNStatusClient((ISVNAuthenticationManager)authManager,null)
352
353            try {
354                // get status of base directory, if this fails exception will be thrown
355                statusClient.doStatus(baseFile, true)
356                updateAndCommitLatest()
357            }
358            catch (SVNException ex) {
359                // error with status, not in repo, attempt import.
360                if (ex.message.contains("is not a working copy")) {
361                    boolean notInRepository = isPluginNotInRepository()
362                    if (notInRepository) {
363                        importToSVN()
364                    }
365                    else {
366                        def result = confirmInput("""
367The current directory is not a working copy and your latest changes won't be committed.
368You need to checkout a working copy and make your changes there.
369Alternatively, would you like to publish a zip-only (no sources) release?""")
370                        if (!result) exit(0)
371
372                        publishZipOnlyRelease()
373                        return
374                    }
375                }
376                else {
377                    event('StatusFinal', ["Failed to stat working directory: ${ex.message}"])
378                    exit(1)
379                }
380            }
381            tagPluginRelease()
382            modifyOrCreatePluginList()
383            event('StatusFinal', ["Plug-in release successfully published"])
384        }
385    }
386    catch(Exception e) {
387        logErrorAndExit("Error occurred with release-plugin", e)
388    }
389}
390
391def publishZipOnlyRelease() {
392    def localWorkingCopy = new File("${projectWorkDir}/working-copy")
393    ant.mkdir(dir: localWorkingCopy)
394
395    if (isPluginNotInRepository()) {
396        updateLocalZipAndXml(localWorkingCopy)
397        importBaseToSVN(localWorkingCopy)
398    }
399    cleanLocalWorkingCopy(localWorkingCopy)
400    checkoutFromSVN(localWorkingCopy, trunk)
401    updateLocalZipAndXml(localWorkingCopy)
402    addPluginZipAndMetadataIfNeccessary(new File("${localWorkingCopy}/plugin.xml"), new File("${localWorkingCopy}/${new File(pluginZip).name}"))
403    commitDirectoryToSVN(localWorkingCopy)
404
405    tagPluginRelease()
406    modifyOrCreatePluginList()
407	deleteZipFromTrunk()
408    println "Successfully published zip-only plugin release."
409}
410
411def deleteZipFromTrunk() {
412    def commitClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null)
413
414    if (!commitMessage) askForMessage()
415	if(pluginZip) {
416		def pluginZipFile = new File(pluginZip)
417		def zipLocation = SVNURL.parseURIDecoded("${remoteLocation}/trunk/${pluginZipFile.name}")
418	    try { commitClient.doDelete([zipLocation] as SVNURL[], commitMessage) }
419	    catch (SVNException e) {
420	        // ok - the zip doesn't exist yet
421	    }
422	}
423}
424
425def updateLocalZipAndXml(File localWorkingCopy) {
426    ant.copy(file: pluginZip, todir: localWorkingCopy, overwrite:true)
427    ant.copy(file: "${basedir}/plugin.xml", todir: localWorkingCopy, overwrite:true)
428}
429
430def cleanLocalWorkingCopy(File localWorkingCopy) {
431    ant.delete(dir: localWorkingCopy)
432    ant.mkdir(dir: localWorkingCopy)
433}
434
435boolean isPluginNotInRepository() {
436    // Now check whether the plugin is in the repository.
437    // If not, we ask the user whether they want to import it.
438    SVNRepository repos = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(pluginSVN))
439    if(authManager != null)
440        repos.authenticationManager = authManager
441    boolean notInRepository = true
442    try {
443        notInRepository = !repos.info("grails-$pluginName", -1)
444    }
445    catch (e) {
446        // ignore
447    }
448    return notInRepository
449}
450
451target(modifyOrCreatePluginList:"Updates the remote plugin.xml descriptor or creates a new one in the repo") {
452    withPluginListUpdate {
453        ant.delete(file:pluginsListFile)
454        // get newest version of plugin list
455        try {
456            fetchRemoteFile("${pluginSVN}/.plugin-meta/plugins-list.xml", pluginsListFile)
457        }
458        catch (Exception e) {
459            println "Error reading remote plugin list [${e.message}], building locally..."
460            updatePluginsListManually()
461        }
462
463        def remoteRevision = "0"
464        if (shouldUseSVNProtocol(pluginDistURL)) {
465            withSVNRepo(pluginDistURL) { repo ->
466                remoteRevision = repo.getLatestRevision().toString()
467            }
468        }
469        else {
470            new URL(pluginDistURL).withReader { Reader reader ->
471                def line = reader.readLine()
472                line.eachMatch(/Revision (.*):/) {
473                    remoteRevision = it[1]
474                }
475            }
476        }
477
478        def publisher = new DefaultPluginPublisher(remoteRevision, pluginDistURL)
479        def updatedList = publisher.publishRelease(pluginName, new FileSystemResource(pluginsListFile), !skipLatest)
480        pluginsListFile.withWriter("UTF-8") { w ->
481            publisher.writePluginList(updatedList, w)
482        }
483    }
484}
485
486target(commitNewGlobalPluginList:"updates the plugins.xml descriptor stored in the repo") {
487    withPluginListUpdate {
488        ant.delete(file:pluginsListFile)
489        println "Building plugin list for commit..."
490        updatePluginsListManually()
491    }
492}
493
494private withPluginListUpdate(Closure updateLogic) {
495    if (!commitMessage) {
496        askForMessage()
497    }
498    updateLogic()
499
500    def pluginMetaDir = new File("${grailsSettings.grailsWorkDir}/${repositoryName}/.plugin-meta")
501    def updateClient = new SVNUpdateClient((ISVNAuthenticationManager)authManager, null)
502    def importClient = new SVNCommitClient((ISVNAuthenticationManager)authManager, null)
503    def addClient = new SVNWCClient((ISVNAuthenticationManager)authManager, null)
504
505    String remotePluginMetadata = "${pluginSVN}/.plugin-meta"
506    if (!pluginMetaDir.exists()) {
507        println "Checking out locally to '${pluginMetaDir}'."
508        checkoutOrImportPluginMetadata(pluginMetaDir, remotePluginMetadata, updateClient, importClient)
509    }
510    else {
511        try {
512            updateClient.doUpdate(pluginMetaDir, SVNRevision.HEAD, true)
513            commitNewestPluginList(pluginMetaDir, importClient)
514        }
515        catch (SVNException e) {
516            println "Plugin meta directory corrupt, checking out again"
517            checkoutOrImportPluginMetadata(pluginMetaDir, remotePluginMetadata, updateClient, importClient)
518        }
519    }
520}
521
522private checkoutOrImportPluginMetadata (File pluginMetaDir, String remotePluginMetadata, SVNUpdateClient updateClient, SVNCommitClient importClient) {
523    def svnURL = SVNURL.parseURIDecoded (remotePluginMetadata)
524    try {
525        updateClient.doCheckout(svnURL, pluginMetaDir, SVNRevision.HEAD, SVNRevision.HEAD, true)
526
527        try {
528            addClient.doAdd(new File("$pluginMetaDir/plugins-list.xml"), false, false, false, false)
529        }
530        catch (e) {
531        // ignore
532        }
533        commitNewestPluginList(pluginMetaDir, importClient)
534    }
535    catch (SVNException e) {
536        println "Importing plugin meta data to ${remotePluginMetadata}. Please wait..."
537
538        ant.mkdir(dir: pluginMetaDir)
539        ant.copy(file: pluginsListFile, tofile: "$pluginMetaDir/plugins-list.xml")
540
541        def commit = importClient.doImport(pluginMetaDir, svnURL, commitMessage, true)
542        println "Committed revision ${commit.newRevision} of plugins-list.xml."
543        ant.delete(dir: pluginMetaDir)
544        updateClient.doCheckout(svnURL, pluginMetaDir, SVNRevision.HEAD, SVNRevision.HEAD, true)
545    }
546}
547
548private def commitNewestPluginList(File pluginMetaDir, SVNCommitClient importClient) {
549    ant.copy(file: pluginsListFile, tofile: "$pluginMetaDir/plugins-list.xml", overwrite: true)
550
551    def commit = importClient.doCommit([pluginMetaDir] as File[], false, commitMessage, true, true)
552
553    println "Committed revision ${commit.newRevision} of plugins-list.xml."
554}
555
556target(checkInPluginZip:"Checks in the plug-in zip if it has not been checked in already") {
557
558    def pluginXml = new File("${basedir}/plugin.xml")
559
560    addPluginZipAndMetadataIfNeccessary(pluginXml, new File(pluginZip))
561}
562
563def addPluginZipAndMetadataIfNeccessary(File pluginXml, File pluginFile) {
564    def statusClient = new SVNStatusClient((ISVNAuthenticationManager) authManager, null)
565    def wcClient = new SVNWCClient((ISVNAuthenticationManager) authManager, null)
566
567    def addPluginFile = false
568    try {
569        def status = statusClient.doStatus(pluginFile, true)
570        if (status.kind == SVNNodeKind.NONE || status.kind == SVNNodeKind.UNKNOWN) addPluginFile = true
571    }
572    catch (SVNException) {
573        // not checked in add and commit
574        addPluginFile = true
575    }
576    if (addPluginFile) wcClient.doAdd(pluginFile, true, false, false, false)
577    addPluginFile = false
578    try {
579        def status = statusClient.doStatus(pluginXml, true)
580        if (status.kind == SVNNodeKind.NONE || status.kind == SVNNodeKind.UNKNOWN) addPluginFile = true
581    }
582    catch (SVNException e) {
583        addPluginFile = true
584    }
585    if (addPluginFile) wcClient.doAdd(pluginXml, true, false, false, false)
586}
587
588target(updateAndCommitLatest:"Commits the latest revision of the Plug-in") {
589   def result = !isInteractive || confirmInput("""
590This command will perform the following steps to release your plug-in to Grails' SVN repository:
591* Update your sources to the HEAD revision
592* Commit any changes you've made to SVN
593* Tag the release
594
595NOTE: This command will not add new resources for you, if you have additional sources to add please run 'svn add' before running this command.
596NOTE: Make sure you have updated the version number in your *GrailsPlugin.groovy descriptor.
597
598Are you sure you wish to proceed?
599""")
600    if (!result) exit(0)
601
602    println "Checking in plugin zip..."
603    checkInPluginZip()
604
605    long r = updateDirectoryFromSVN(baseFile)
606    println "Updated to revision ${r}. Committing local, please wait..."
607
608    def commit = commitDirectoryToSVN(baseFile)
609
610    println "Committed revision ${commit.newRevision}."
611}
612
613def commitDirectoryToSVN(baseFile) {
614    commitClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null)
615
616    if (!commitMessage) askForMessage()
617
618    println "Committing code. Please wait..."
619
620    return commitClient.doCommit([baseFile] as File[], false, commitMessage, true, true)
621}
622
623long updateDirectoryFromSVN(baseFile) {
624    updateClient = new SVNUpdateClient((ISVNAuthenticationManager) authManager, null)
625
626    println "Updating from SVN..."
627    long r = updateClient.doUpdate(baseFile, SVNRevision.HEAD, true)
628    return r
629}
630
631target(importToSVN:"Imports a plugin project to Grails' remote SVN repository") {
632    File checkOutDir = new File("${baseFile.parentFile.absolutePath}/checkout/${baseFile.name}")
633
634    ant.unzip(src:pluginZip, dest:"${basedir}/unzipped")
635    ant.copy(file:pluginZip, todir:"${basedir}/unzipped")
636    ant.mkdir(dir:"${basedir}/unzipped/grails-app")
637
638    File importBaseDirectory = new File("${basedir}/unzipped")
639
640    String testsDir = "${importBaseDirectory}/test"
641    ant.mkdir(dir:testsDir)
642    ant.copy(todir:testsDir) {
643        fileset(dir:"${grailsSettings.testSourceDir}")
644    }
645
646    try {
647        def result = !isInteractive || confirmInput("""
648    This plug-in project is not currently in the repository, this command will now:
649    * Perform an SVN import into the repository
650    * Checkout the imported version of the project from SVN to '${checkOutDir}'
651    * Tag the plug-in project as the LATEST_RELEASE
652    Are you sure you wish to proceed?
653        """)
654        if (!result) {
655            ant.delete(dir:importBaseDirectory, failonerror:false)
656            exit(0)
657        }
658        svnURL = importBaseToSVN(importBaseDirectory)
659    }
660    finally {
661        ant.delete(dir:importBaseDirectory, failonerror:false)
662    }
663    checkOutDir.parentFile.mkdirs()
664
665    checkoutFromSVN(checkOutDir, svnURL)
666
667    event('StatusFinal', ["""
668Completed SVN project import. If you are in terminal navigate to imported project with:
669cd ${checkOutDir}
670
671Future changes should be made to the SVN controlled sources!"""])
672}
673
674def checkoutFromSVN(File checkOutDir, SVNURL svnURL) {
675    updateClient = new SVNUpdateClient((ISVNAuthenticationManager) authManager, null)
676    println "Checking out locally to '${checkOutDir}'."
677    updateClient.doCheckout(svnURL, checkOutDir, SVNRevision.HEAD, SVNRevision.HEAD, true)
678}
679
680SVNURL importBaseToSVN(File importBaseDirectory) {
681    importClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null)
682    if (!commitMessage) askForMessage()
683
684    println "Importing project to ${remoteLocation}. Please wait..."
685
686    def svnURL = SVNURL.parseURIDecoded("${remoteLocation}/trunk")
687    importClient.doImport(importBaseDirectory, svnURL, commitMessage, true)
688    println "Plug-in project imported to SVN at location '${remoteLocation}/trunk'"
689    return svnURL
690}
691
692target(tagPluginRelease:"Tags a plugin-in with the LATEST_RELEASE tag and version tag within the /tags area of SVN") {
693    println "Preparing to publish the release..."
694
695    def copyClient = new SVNCopyClient((ISVNAuthenticationManager) authManager, null)
696    def commitClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null)
697
698    if (!commitMessage) askForMessage()
699
700    tags = SVNURL.parseURIDecoded("${remoteLocation}/tags")
701    latest = SVNURL.parseURIDecoded(latestRelease)
702    release = SVNURL.parseURIDecoded(versionedRelease)
703
704    try { commitClient.doMkDir([tags] as SVNURL[], commitMessage) }
705    catch (SVNException e) {
706    // ok - already exists
707    }
708    if (!skipLatest) {
709        try { commitClient.doDelete([latest] as SVNURL[], commitMessage) }
710        catch (SVNException e) {
711            // ok - the tag doesn't exist yet
712        }
713    }
714    try { commitClient.doDelete([release] as SVNURL[], commitMessage) }
715    catch (SVNException e) {
716        // ok - the tag doesn't exist yet
717    }
718
719    // Get remote URL for this working copy.
720    def wcClient = new SVNWCClient((ISVNAuthenticationManager) authManager, null)
721    def copyFromUrl = trunk
722
723    // First tag this release with the version number.
724    try {
725        println "Tagging version release, please wait..."
726        def copySource = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, copyFromUrl)
727        def commit = copyClient.doCopy([copySource] as SVNCopySource[], release, false, false, true, commitMessage, new SVNProperties())
728        println "Copied trunk to ${versionedRelease} with revision ${commit.newRevision} on ${commit.date}"
729
730        // And now make it the latest release.
731        if (!skipLatest) {
732            println "Tagging latest release, please wait..."
733            copySource = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, release)
734            commit = copyClient.doCopy([copySource] as SVNCopySource[], latest, false, false, true, commitMessage, new SVNProperties())
735            println "Copied trunk to ${latestRelease} with revision ${commit.newRevision} on ${commit.date}"
736        }
737    }
738    catch (SVNException e) {
739        logErrorAndExit("Error tagging release", e)
740    }
741}
742
743target(askForMessage:"Asks for the users commit message") {
744    ant.input(message:"Enter a SVN commit message:", addproperty:"commit.message")
745    commitMessage = ant.antProject.properties."commit.message"
746}
747
748target(updatePluginsListManually: "Updates the plugin list by manually reading each URL, the slow way") {
749    depends(configureProxy)
750    try {
751        def recreateCache = false
752        document = null
753        if (!pluginsListFile.exists()) {
754            println "Plugins list cache doesn't exist creating.."
755            recreateCache = true
756        }
757        else {
758            try {
759                document = DOMBuilder.parse(new FileReader(pluginsListFile))
760            }
761            catch (Exception e) {
762                recreateCache = true
763                println "Plugins list cache is corrupt [${e.message}]. Re-creating.."
764            }
765        }
766        if (recreateCache) {
767            document = DOMBuilder.newInstance().createDocument()
768            def root = document.createElement('plugins')
769            root.setAttribute('revision', '0')
770            document.appendChild(root)
771        }
772
773        pluginsList = document.documentElement
774        builder = new DOMBuilder(document)
775
776        def localRevision = pluginsList ? new Integer(pluginsList.getAttribute('revision')) : -1
777        // extract plugins svn repository revision - used for determining cache up-to-date
778        def remoteRevision = 0
779        try {
780            // determine if this is a secure plugin spot..
781            if (shouldUseSVNProtocol(pluginDistURL)) {
782                withSVNRepo(pluginDistURL) { repo ->
783                    remoteRevision = repo.getLatestRevision()
784                    if (remoteRevision > localRevision) {
785                        // Plugins list cache is expired, need to update
786                        event("StatusUpdate", ["Plugins list cache has expired. Updating, please wait"])
787                        pluginsList.setAttribute('revision', remoteRevision as String)
788                        repo.getDir('', -1,null,(Collection)null).each() { entry ->
789                            final String PREFIX = "grails-"
790                            if (entry.name.startsWith(PREFIX)) {
791                                def pluginName = entry.name.substring(PREFIX.length())
792                                buildPluginInfo(pluginsList, pluginName)
793                            }
794                        }
795                    }
796                }
797            }
798            else {
799                new URL(pluginDistURL).withReader { Reader reader ->
800                    def line = reader.readLine()
801                    line.eachMatch(/Revision (.*):/) {
802                        remoteRevision = it[1].toInteger()
803                    }
804                    if (remoteRevision > localRevision) {
805                        // Plugins list cache is expired, need to update
806                        event("StatusUpdate", ["Plugins list cache has expired. Updating, please wait"])
807                        pluginsList.setAttribute('revision', remoteRevision as String)
808                        // for each plugin directory under Grails Plugins SVN in form of 'grails-*'
809                        while (line = reader.readLine()) {
810                            line.eachMatch(/<li><a href="grails-(.+?)">/) {
811                                // extract plugin name
812                                def pluginName = it[1][0..-2]
813                                // collect information about plugin
814                                buildPluginInfo(pluginsList, pluginName)
815                            }
816                        }
817                    }
818                }
819            }
820        }
821        catch (Exception e) {
822            event("StatusError", ["Unable to list plugins, please check you have a valid internet connection: ${e.message}" ])
823        }
824
825        // update plugins list cache file
826        writePluginsFile()
827    }
828    catch (Exception e) {
829        event("StatusError", ["Unable to list plugins, please check you have a valid internet connection: ${e.message}" ])
830    }
831}
832
833def buildPluginInfo(root, pluginName) {
834    use(DOMCategory) {
835        // determine the plugin node..
836        def pluginNode = root.'plugin'.find {it.'@name' == pluginName}
837        if (!pluginNode) {
838            // add it if it doesn't exist..
839            pluginNode = builder.'plugin'(name: pluginName)
840            root.appendChild(pluginNode)
841        }
842
843        // add each of the releases..
844        event("StatusUpdate", ["Reading [$pluginName] plugin info"])
845        // determine if this is a secure plugin spot..
846        def tagsUrl = "${pluginDistURL}/grails-${pluginName}/tags"
847        try {
848            if (shouldUseSVNProtocol(pluginDistURL)) {
849                withSVNRepo(tagsUrl) { repo ->
850                    repo.getDir('',-1,null,(Collection)null).each() { entry ->
851                        buildReleaseInfo(pluginNode, pluginName, tagsUrl, entry.name)
852                    }
853                }
854            }
855            else {
856                def releaseTagsList = new URL(tagsUrl).text
857                releaseTagsList.eachMatch(/<li><a href="(.+?)">/) {
858                    def releaseTag = it[1][0..-2]
859                    buildReleaseInfo(pluginNode, pluginName, tagsUrl, releaseTag)
860                }
861            }
862        }
863        catch(e) {
864            // plugin has not tags
865            println "Plugin [$pluginName] doesn't have any tags"
866        }
867
868        try {
869            def latestRelease = null
870            def url = "${pluginDistURL}/grails-${pluginName}/tags/LATEST_RELEASE/plugin.xml"
871            fetchPluginListFile(url).withReader {Reader reader ->
872                def line = reader.readLine()
873                line.eachMatch (/.+?version='(.+?)'.+/) {
874                    latestRelease = it[1]
875                }
876            }
877            if (latestRelease && pluginNode.'release'.find {it.'@version' == latestRelease}) {
878                pluginNode.setAttribute('latest-release', latestRelease as String)
879            }
880        }
881        catch(e) {
882            // plugin doesn't have a latest release
883            println "Plugin [$pluginName] doesn't have a latest release"
884        }
885    }
886}
887
888def buildReleaseInfo(root, pluginName, releasePath, releaseTag) {
889    // quick exit nothing to do..
890    if (releaseTag == '..' || releaseTag == 'LATEST_RELEASE') {
891        return
892    }
893
894    // remove previous version..
895    def releaseNode = root.'release'.find() {
896        it.'@tag' == releaseTag && it.'&type' == 'svn'
897    }
898    if (releaseNode) {
899        root.removeChild(releaseNode)
900    }
901    try {
902
903        // copy the properties to the new node..
904        def releaseUrl = "${releasePath}/${releaseTag}"
905        def properties = ['title', 'author', 'authorEmail', 'description', 'documentation']
906        def releaseDescriptor = parseRemoteXML("${releaseUrl}/plugin.xml").documentElement
907        def version = releaseDescriptor.'@version'
908
909        releaseNode = builder.createNode('release', [tag: releaseTag, version: version, type: 'svn'])
910        root.appendChild(releaseNode)
911        properties.each {
912            if (releaseDescriptor."${it}") {
913                releaseNode.appendChild(builder.createNode(it, releaseDescriptor."${it}".text()))
914            }
915        }
916        releaseNode.appendChild(builder.createNode('file', "${releaseUrl}/grails-${pluginName}-${version}.zip"))
917    }
918    catch(e) {
919        // no release info available, probably an older plugin with no plugin.xml defined.
920    }
921}
922
923def parsePluginList() {
924    if (pluginsList == null) {
925        profile("Reading local plugin list from $pluginsListFile") {
926            def document
927            try {
928                document = DOMBuilder.parse(new FileReader(pluginsListFile))
929            }
930            catch (Exception e) {
931                println "Plugin list file corrupt, retrieving again.."
932                readRemotePluginList()
933                document = DOMBuilder.parse(new FileReader(pluginsListFile))
934            }
935            pluginsList = document.documentElement
936        }
937    }
938}
939
940def readRemotePluginList() {
941    ant.delete(file:pluginsListFile, failonerror:false)
942    ant.mkdir(dir:pluginsListFile.parentFile)
943    fetchRemoteFile(remotePluginList, pluginsListFile)
944}
945
946def writePluginsFile() {
947    pluginsListFile.parentFile.mkdirs()
948
949    Transformer transformer = TransformerFactory.newInstance().newTransformer()
950    transformer.setOutputProperty(OutputKeys.INDENT, "true")
951    transformer.transform(new DOMSource(document), new StreamResult(pluginsListFile))
952}
953
954shouldUseSVNProtocol = { pluginDistURL ->
955    return isSecureUrl(pluginDistURL) || pluginDistURL.startsWith("file://")
956}
957
958def parseRemoteXML(url) {
959    fetchPluginListFile(url).withReader() { DOMBuilder.parse(it) }
960}
961
962/**
963 * Downloads a remote plugin zip into the plugins dir
964 */
965downloadRemotePlugin = { url, pluginsBase ->
966    def slash = url.file.lastIndexOf('/')
967    def fullPluginName = "${url.file[slash + 8..-5]}"
968    String zipLocation = "${pluginsBase}/grails-${fullPluginName}.zip"
969    fetchRemoteFile("${url}", zipLocation)
970    readMetadataFromZip(zipLocation, url)
971    return fullPluginName
972}
973
974fetchRemoteFile = { url, destfn ->
975    if (shouldUseSVNProtocol(pluginDistURL)) {
976        // fetch the remote file..
977        fetchRemote(url) { repo, file ->
978            // get the latest file from the repository..
979            def f = (destfn instanceof File) ? destfn : new File(destfn)
980            f.withOutputStream() { os ->
981                def props = new SVNProperties()
982                repo.getFile(file , (long)-1L, props , os)
983            }
984        }
985    }
986    else {
987        ant.get(src:url, dest:destfn, verbose:"yes", usetimestamp:true)
988    }
989}
990
991/**
992 * Fetch the entire plugin list file.
993 */
994fetchPluginListFile = { url ->
995    // attempt to fetch the file using SVN.
996    if (shouldUseSVNProtocol(pluginDistURL)) {
997        def rdr = fetchRemote(url) { repo, file ->
998            // get the latest file from the repository..
999            def props = new SVNProperties()
1000            def baos = new ByteArrayOutputStream()
1001            def ver = repo.getFile(file , (long)-1L, props , baos)
1002            def mimeType = props.getSVNPropertyValue(SVNProperty.MIME_TYPE)
1003            if (!SVNProperty.isTextMimeType(mimeType)) {
1004                throw new Exception("Must be a text file..")
1005            }
1006            return new StringReader(new String(baos.toByteArray(), 'utf-8'))
1007        }
1008        return rdr
1009    }
1010    // attempt using URL
1011    return new URL(url)
1012}
1013
1014def fetchRemote(url, closure) {
1015    def idx = url.lastIndexOf('/')
1016    def svnUrl = url.substring(0,idx)
1017    def file = url.substring(idx+1,url.length())
1018
1019    withSVNRepo(svnUrl) { repo ->
1020        // determine if the file exists
1021        SVNNodeKind nodeKind = repo.checkPath(file , -1)
1022        if (nodeKind == SVNNodeKind.NONE) {
1023            throw new Exception("The file does not exist.: " + url)
1024        }
1025        if (nodeKind != SVNNodeKind.FILE) {
1026            throw new Exception("Error not a file..: " + url)
1027        }
1028        // present w/ file etc for repo extraction
1029        closure.call(repo, file)
1030    }
1031}
1032
1033def isSecureUrl(Object url) {
1034    url.startsWith('https://') || url.startsWith('svn://')
1035}
1036
1037withSVNRepo = { url, closure ->
1038    // create a authetication manager using the defaults
1039    ISVNAuthenticationManager authMgr = getAuthFromUrl(url,"discovery")
1040
1041    // create the url
1042    def svnUrl = SVNURL.parseURIEncoded(url)
1043    def repo = SVNRepositoryFactory.create(svnUrl, null)
1044    repo.authenticationManager = authMgr
1045    // trigger authentication failure?.?.
1046    try {
1047        repo.getLatestRevision()
1048    }
1049    catch (SVNAuthenticationException ex) {
1050        event "StatusUpdate", ["Default authentication failed please enter credentials."]
1051        // prompt for login information..
1052        ant.input(message:"Please enter your SVN username:", addproperty:"user.svn.username")
1053        ant.input(message:"Please enter your SVN password:", addproperty:"user.svn.password")
1054        def username = ant.antProject.properties."user.svn.username"
1055        def password = ant.antProject.properties."user.svn.password"
1056        authMgr = SVNWCUtil.createDefaultAuthenticationManager(username , password)
1057        repo.setAuthenticationManager(authMgr)
1058        // don't bother to catch this one let it bubble up..
1059        repo.getLatestRevision()
1060    }
1061    // make sure the closure return is returned..
1062    closure.call(repo)
1063}
1064